Really-amin commited on
Commit
e19c89a
·
verified ·
1 Parent(s): 76dcb98

Upload 299 files

Browse files
Files changed (3) hide show
  1. api_server_extended.py +113 -39
  2. static/js/app.js +411 -197
  3. templates/index.html +117 -50
api_server_extended.py CHANGED
@@ -983,43 +983,83 @@ async def get_providers_auto_discovery_report():
983
 
984
  @app.get("/api/providers/health-summary")
985
  async def get_providers_health_summary():
986
- """Get simplified health summary from auto-discovery report"""
987
- report = load_auto_discovery_report()
988
-
989
- if not report or "stats" not in report:
990
- return {
991
- "ok": False,
992
- "error": "Auto-discovery report not found or invalid",
993
- "message": f"Report file not found at {AUTO_DISCOVERY_REPORT_PATH}"
994
- }
995
-
996
- stats = report.get("stats", {})
997
- http_providers = report.get("http_providers", {})
998
- hf_providers = report.get("hf_providers", {})
999
-
1000
- # Count by status
1001
- status_counts = {"VALID": 0, "INVALID": 0, "CONDITIONALLY_AVAILABLE": 0}
1002
- for result in http_providers.get("results", []):
1003
- status = result.get("status", "UNKNOWN")
1004
- if status in status_counts:
1005
- status_counts[status] += 1
1006
-
1007
- return {
1008
- "ok": True,
1009
- "summary": {
1010
- "total_active_providers": stats.get("total_active_providers", 0),
1011
- "http_valid": stats.get("http_valid", 0),
1012
- "http_invalid": stats.get("http_invalid", 0),
1013
- "http_conditional": stats.get("http_conditional", 0),
1014
- "hf_valid": stats.get("hf_valid", 0),
1015
- "hf_invalid": stats.get("hf_invalid", 0),
1016
- "hf_conditional": stats.get("hf_conditional", 0),
1017
- "status_breakdown": status_counts,
1018
- "execution_time_sec": stats.get("execution_time_sec", 0),
1019
- "timestamp": stats.get("timestamp", "")
1020
- },
1021
- "source": "PROVIDER_AUTO_DISCOVERY_REPORT.json"
1022
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
 
1024
  @app.get("/api/apl/summary")
1025
  async def get_apl_summary():
@@ -1492,25 +1532,59 @@ async def get_latest_news(
1492
 
1493
  @app.get("/api/models/status")
1494
  async def get_models_status():
1495
- """Get AI models status and registry info"""
1496
  try:
1497
- from ai_models import get_model_info, registry_status, initialize_models
1498
 
1499
  model_info = get_model_info()
1500
  registry_info = registry_status()
1501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1502
  return {
1503
  "success": True,
 
 
 
 
 
 
 
1504
  "models": model_info,
1505
  "registry": registry_info,
 
 
1506
  "database": {
1507
  "path": str(DB_PATH),
1508
  "exists": DB_PATH.exists()
1509
  }
1510
  }
1511
  except Exception as e:
 
1512
  return {
1513
  "success": False,
 
 
1514
  "error": str(e)
1515
  }
1516
 
 
983
 
984
  @app.get("/api/providers/health-summary")
985
  async def get_providers_health_summary():
986
+ """Get simplified health summary from auto-discovery report - always returns 200"""
987
+ try:
988
+ report = load_auto_discovery_report()
989
+
990
+ if not report or "stats" not in report:
991
+ return JSONResponse(
992
+ status_code=200,
993
+ content={
994
+ "ok": False,
995
+ "error": "Auto-discovery report not found or invalid",
996
+ "message": f"Report file not found at {AUTO_DISCOVERY_REPORT_PATH}",
997
+ "summary": {
998
+ "total_active_providers": 0,
999
+ "http_valid": 0,
1000
+ "http_invalid": 0,
1001
+ "http_conditional": 0,
1002
+ "hf_valid": 0,
1003
+ "hf_invalid": 0,
1004
+ "hf_conditional": 0,
1005
+ "status_breakdown": {"VALID": 0, "INVALID": 0, "CONDITIONALLY_AVAILABLE": 0},
1006
+ "execution_time_sec": 0,
1007
+ "timestamp": ""
1008
+ }
1009
+ }
1010
+ )
1011
+
1012
+ stats = report.get("stats", {})
1013
+ http_providers = report.get("http_providers", {})
1014
+ hf_providers = report.get("hf_providers", {})
1015
+
1016
+ # Count by status
1017
+ status_counts = {"VALID": 0, "INVALID": 0, "CONDITIONALLY_AVAILABLE": 0}
1018
+ for result in http_providers.get("results", []):
1019
+ status = result.get("status", "UNKNOWN")
1020
+ if status in status_counts:
1021
+ status_counts[status] += 1
1022
+
1023
+ return JSONResponse(
1024
+ status_code=200,
1025
+ content={
1026
+ "ok": True,
1027
+ "summary": {
1028
+ "total_active_providers": stats.get("total_active_providers", 0),
1029
+ "http_valid": stats.get("http_valid", 0),
1030
+ "http_invalid": stats.get("http_invalid", 0),
1031
+ "http_conditional": stats.get("http_conditional", 0),
1032
+ "hf_valid": stats.get("hf_valid", 0),
1033
+ "hf_invalid": stats.get("hf_invalid", 0),
1034
+ "hf_conditional": stats.get("hf_conditional", 0),
1035
+ "status_breakdown": status_counts,
1036
+ "execution_time_sec": stats.get("execution_time_sec", 0),
1037
+ "timestamp": stats.get("timestamp", "")
1038
+ },
1039
+ "source": "PROVIDER_AUTO_DISCOVERY_REPORT.json"
1040
+ }
1041
+ )
1042
+ except Exception as e:
1043
+ logger.error(f"Error loading health summary: {e}")
1044
+ return JSONResponse(
1045
+ status_code=200,
1046
+ content={
1047
+ "ok": False,
1048
+ "error": str(e),
1049
+ "summary": {
1050
+ "total_active_providers": 0,
1051
+ "http_valid": 0,
1052
+ "http_invalid": 0,
1053
+ "http_conditional": 0,
1054
+ "hf_valid": 0,
1055
+ "hf_invalid": 0,
1056
+ "hf_conditional": 0,
1057
+ "status_breakdown": {"VALID": 0, "INVALID": 0, "CONDITIONALLY_AVAILABLE": 0},
1058
+ "execution_time_sec": 0,
1059
+ "timestamp": ""
1060
+ }
1061
+ }
1062
+ )
1063
 
1064
  @app.get("/api/apl/summary")
1065
  async def get_apl_summary():
 
1532
 
1533
  @app.get("/api/models/status")
1534
  async def get_models_status():
1535
+ """Get AI models status and registry info - honest status reporting"""
1536
  try:
1537
+ from ai_models import get_model_info, registry_status, HF_MODE, TRANSFORMERS_AVAILABLE, _registry
1538
 
1539
  model_info = get_model_info()
1540
  registry_info = registry_status()
1541
 
1542
+ # Determine honest status
1543
+ if HF_MODE == "off":
1544
+ status = "disabled"
1545
+ status_message = "HF models are disabled (HF_MODE=off). To enable them, set HF_MODE=public or HF_MODE=auth in the environment."
1546
+ elif not TRANSFORMERS_AVAILABLE:
1547
+ status = "transformers_unavailable"
1548
+ status_message = "Transformers library is not installed. Models cannot be loaded."
1549
+ elif not _registry._initialized:
1550
+ status = "not_initialized"
1551
+ status_message = "Models have not been initialized yet."
1552
+ elif len(_registry._pipelines) == 0:
1553
+ status = "no_models_loaded"
1554
+ status_message = f"No models could be loaded. {len(_registry._failed_models)} models failed. Check model IDs or HF access."
1555
+ elif len(_registry._pipelines) > 0:
1556
+ status = "ok" if len(_registry._failed_models) == 0 else "partial"
1557
+ status_message = f"{len(_registry._pipelines)} model(s) loaded successfully"
1558
+ if len(_registry._failed_models) > 0:
1559
+ status_message += f", {len(_registry._failed_models)} failed"
1560
+ else:
1561
+ status = "unknown"
1562
+ status_message = "Unknown status"
1563
+
1564
  return {
1565
  "success": True,
1566
+ "status": status,
1567
+ "status_message": status_message,
1568
+ "hf_mode": HF_MODE,
1569
+ "models_loaded": len(_registry._pipelines),
1570
+ "models_failed": len(_registry._failed_models),
1571
+ "transformers_available": TRANSFORMERS_AVAILABLE,
1572
+ "initialized": _registry._initialized,
1573
  "models": model_info,
1574
  "registry": registry_info,
1575
+ "failed_models": list(_registry._failed_models.keys())[:10], # Limit for brevity
1576
+ "loaded_models": list(_registry._pipelines.keys()),
1577
  "database": {
1578
  "path": str(DB_PATH),
1579
  "exists": DB_PATH.exists()
1580
  }
1581
  }
1582
  except Exception as e:
1583
+ logger.error(f"Error getting models status: {e}")
1584
  return {
1585
  "success": False,
1586
+ "status": "error",
1587
+ "status_message": f"Error retrieving model status: {str(e)}",
1588
  "error": str(e)
1589
  }
1590
 
static/js/app.js CHANGED
@@ -130,7 +130,7 @@ function loadAPIEndpoints() {
130
  ];
131
 
132
  // Clear existing options except first one
133
- endpointSelect.innerHTML = '<option value="">انتخاب Endpoint...</option>';
134
  endpoints.forEach(ep => {
135
  const option = document.createElement('option');
136
  option.value = ep.value;
@@ -148,15 +148,15 @@ async function checkAPIStatus() {
148
  const statusBadge = document.getElementById('api-status');
149
  if (data.status === 'healthy') {
150
  statusBadge.className = 'status-badge';
151
- statusBadge.innerHTML = '<span class="status-dot"></span><span>✅ سیستم فعال</span>';
152
  } else {
153
  statusBadge.className = 'status-badge error';
154
- statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ خطا</span>';
155
  }
156
  } catch (error) {
157
  const statusBadge = document.getElementById('api-status');
158
  statusBadge.className = 'status-badge error';
159
- statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ اتصال برقرار نشد</span>';
160
  }
161
  }
162
 
@@ -188,12 +188,12 @@ async function loadDashboard() {
188
 
189
  systemStatusDiv.innerHTML = `
190
  <div class="alert ${healthClass}">
191
- <strong>وضعیت سیستم:</strong> ${healthStatus}<br>
192
- <strong>APIهای آنلاین:</strong> ${statusData.online || 0}<br>
193
- <strong>APIهای تخریب شده:</strong> ${statusData.degraded || 0}<br>
194
- <strong>APIهای آفلاین:</strong> ${statusData.offline || 0}<br>
195
- <strong>میانگین زمان پاسخ:</strong> ${statusData.avg_response_time_ms || 0}ms<br>
196
- <strong>آخرین به‌روزرسانی:</strong> ${new Date(statusData.last_update || Date.now()).toLocaleString('fa-IR')}
197
  </div>
198
  `;
199
  } catch (statusError) {
@@ -207,7 +207,7 @@ async function loadDashboard() {
207
  }
208
  } catch (error) {
209
  console.error('Error loading dashboard:', error);
210
- showError('خطا در بارگذاری داشبورد');
211
  }
212
  }
213
 
@@ -225,7 +225,7 @@ function createCategoriesChart(categories) {
225
  data: {
226
  labels: Object.keys(categories),
227
  datasets: [{
228
- label: 'تعداد منابع',
229
  data: Object.values(categories),
230
  backgroundColor: 'rgba(102, 126, 234, 0.6)',
231
  borderColor: 'rgba(102, 126, 234, 1)',
@@ -258,11 +258,11 @@ async function loadMarketData() {
258
  <thead>
259
  <tr>
260
  <th>#</th>
261
- <th>نام</th>
262
- <th>قیمت (USD)</th>
263
- <th>تغییر 24h</th>
264
- <th>حجم 24h</th>
265
- <th>مارکت کپ</th>
266
  </tr>
267
  </thead>
268
  <tbody>
@@ -285,12 +285,12 @@ async function loadMarketData() {
285
  </table>
286
  </div>
287
  ${data.total_market_cap ? `<div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
288
- <strong>کل مارکت کپ:</strong> $${formatNumber(data.total_market_cap)} |
289
  <strong>BTC Dominance:</strong> ${(data.btc_dominance || 0).toFixed(2)}%
290
  </div>` : ''}
291
  `;
292
  } else {
293
- document.getElementById('market-data').innerHTML = '<div class="alert alert-warning">داده‌ای یافت نشد</div>';
294
  }
295
 
296
  // Load trending
@@ -308,7 +308,7 @@ async function loadMarketData() {
308
  <span style="font-size: 18px; font-weight: 800; color: var(--primary);">#${index + 1}</span>
309
  <div>
310
  <strong>${coin.symbol || coin.id}</strong> - ${coin.name || 'Unknown'}
311
- ${coin.market_cap_rank ? `<div style="font-size: 12px; color: var(--text-secondary);">رتبه مارکت کپ: ${coin.market_cap_rank}</div>` : ''}
312
  </div>
313
  </div>
314
  <div style="font-size: 20px; font-weight: 700; color: var(--success);">${coin.score ? coin.score.toFixed(2) : 'N/A'}</div>
@@ -317,11 +317,11 @@ async function loadMarketData() {
317
  </div>
318
  `;
319
  } else {
320
- document.getElementById('trending-coins').innerHTML = '<div class="alert alert-warning">داده‌ای یافت نشد</div>';
321
  }
322
  } catch (trendingError) {
323
  console.warn('Trending endpoint error:', trendingError);
324
- document.getElementById('trending-coins').innerHTML = '<div class="alert alert-error">خطا در بارگذاری ارزهای ترند</div>';
325
  }
326
 
327
  // Load Fear & Greed
@@ -350,23 +350,23 @@ async function loadMarketData() {
350
  ${fgLabel}
351
  </div>
352
  <div style="font-size: 14px; color: var(--text-secondary);">
353
- شاخص ترس و طمع بازار
354
  </div>
355
  ${sentimentData.timestamp ? `<div style="font-size: 12px; color: var(--text-secondary); margin-top: 10px;">
356
- آخرین به‌روزرسانی: ${new Date(sentimentData.timestamp).toLocaleString('fa-IR')}
357
  </div>` : ''}
358
  </div>
359
  `;
360
  } else {
361
- document.getElementById('fear-greed').innerHTML = '<div class="alert alert-warning">داده‌ای یافت نشد</div>';
362
  }
363
  } catch (sentimentError) {
364
  console.warn('Sentiment endpoint error:', sentimentError);
365
- document.getElementById('fear-greed').innerHTML = '<div class="alert alert-error">خطا در بارگذاری شاخص ترس و طمع</div>';
366
  }
367
  } catch (error) {
368
  console.error('Error loading market data:', error);
369
- showError('خطا در بارگذاری داده‌های بازار');
370
  }
371
  }
372
 
@@ -406,13 +406,13 @@ async function loadModels() {
406
  <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">
407
  ${model.task || model.category || 'N/A'}
408
  </div>
409
- ${model.category ? `<div style="font-size: 11px; color: var(--text-secondary);">دسته: ${model.category}</div>` : ''}
410
  ${model.requires_auth !== undefined ? `<div style="font-size: 11px; color: var(--text-secondary);">
411
- ${model.requires_auth ? '🔐 نیاز به احراز هویت' : '🔓 بدون احراز هویت'}
412
  </div>` : ''}
413
  </div>
414
  <span style="background: ${statusBg}; color: ${statusColor}; padding: 5px 10px; border-radius: 5px; font-size: 12px; font-weight: 600;">
415
- ${isAvailable ? '✅ در دسترس' : '❌ غیرفعال'}
416
  </span>
417
  </div>
418
  ${model.key ? `<div style="margin-top: 10px; font-size: 11px; color: var(--text-secondary); font-family: monospace;">
@@ -424,7 +424,7 @@ async function loadModels() {
424
  </div>
425
  `;
426
  } else {
427
- document.getElementById('models-list').innerHTML = '<div class="alert alert-warning">هیچ مدلی یافت نشد</div>';
428
  }
429
 
430
  // Load models status
@@ -433,18 +433,37 @@ async function loadModels() {
433
  const statusData = await statusRes.json();
434
 
435
  const statusDiv = document.getElementById('models-status');
436
- const status = statusData.status || statusData.ok ? 'فعال' : 'غیرفعال';
437
- const statusClass = statusData.status === 'ok' || statusData.ok ? 'alert-success' : 'alert-warning';
438
-
439
- statusDiv.innerHTML = `
440
- <div class="alert ${statusClass}">
441
- <strong>وضعیت:</strong> ${status}<br>
442
- <strong>مدل‌های بارگذاری شده:</strong> ${statusData.models_loaded || statusData.pipelines_loaded || 0}<br>
443
- <strong>مدل‌های در دسترس:</strong> ${statusData.models_available || statusData.available_models?.length || 0}<br>
444
- ${statusData.mode ? `<strong>حالت:</strong> ${statusData.mode}<br>` : ''}
445
- ${statusData.transformers_available !== undefined ? `<strong>Transformers:</strong> ${statusData.transformers_available ? '' : '❌'}<br>` : ''}
446
- </div>
447
- `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  } catch (statusError) {
449
  console.warn('Models status endpoint error:', statusError);
450
  }
@@ -460,16 +479,16 @@ async function loadModels() {
460
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
461
  <div style="padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
462
  <div style="font-size: 28px; font-weight: 800; color: var(--primary);">${statsData.statistics.total_analyses || 0}</div>
463
- <div style="font-size: 14px; color: var(--text-secondary);">کل تحلیل‌ها</div>
464
  </div>
465
  <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px;">
466
  <div style="font-size: 28px; font-weight: 800; color: var(--success);">${statsData.statistics.unique_symbols || 0}</div>
467
- <div style="font-size: 14px; color: var(--text-secondary);">نمادهای منحصر به فرد</div>
468
  </div>
469
  ${statsData.statistics.most_used_model ? `
470
  <div style="padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 10px;">
471
  <div style="font-size: 18px; font-weight: 800; color: var(--warning);">${statsData.statistics.most_used_model}</div>
472
- <div style="font-size: 14px; color: var(--text-secondary);">پراستفاده‌ترین مدل</div>
473
  </div>
474
  ` : ''}
475
  </div>
@@ -480,7 +499,7 @@ async function loadModels() {
480
  }
481
  } catch (error) {
482
  console.error('Error loading models:', error);
483
- showError('خطا در بارگذاری مدل‌ها');
484
  }
485
  }
486
 
@@ -491,13 +510,13 @@ async function initializeModels() {
491
  const data = await response.json();
492
 
493
  if (data.success) {
494
- showSuccess('مدل‌ها با موفقیت بارگذاری شدند');
495
  loadModels();
496
  } else {
497
- showError(data.error || 'خطا در بارگذاری مدل‌ها');
498
  }
499
  } catch (error) {
500
- showError('خطا در بارگذاری مدل‌ها: ' + error.message);
501
  }
502
  }
503
 
@@ -509,7 +528,7 @@ async function loadSentimentModels() {
509
 
510
  const models = data.models || data || [];
511
  const select = document.getElementById('sentiment-model');
512
- select.innerHTML = '<option value="">انتخاب مدل...</option>';
513
 
514
  models.filter(m => {
515
  const status = m.status || 'unknown';
@@ -527,21 +546,21 @@ async function loadSentimentModels() {
527
  if (select.options.length === 1) {
528
  const option = document.createElement('option');
529
  option.value = '';
530
- option.textContent = 'هیچ مدلی در دسترس نیست';
531
  option.disabled = true;
532
  select.appendChild(option);
533
  }
534
  } catch (error) {
535
  console.error('Error loading sentiment models:', error);
536
  const select = document.getElementById('sentiment-model');
537
- select.innerHTML = '<option value="">خطا در بارگذاری مدل‌ها</option>';
538
  }
539
  }
540
 
541
  // Analyze Global Market Sentiment
542
  async function analyzeGlobalSentiment() {
543
  const resultDiv = document.getElementById('global-sentiment-result');
544
- resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل احساسات بازار...</div>';
545
 
546
  try {
547
  // Use market text analysis with sample market-related text
@@ -558,7 +577,7 @@ async function analyzeGlobalSentiment() {
558
  if (!data.available) {
559
  resultDiv.innerHTML = `
560
  <div class="alert alert-warning">
561
- <strong>⚠️ مدل‌ها در دسترس نیستند:</strong> ${data.error || 'مدل‌های AI در حال حاضر در دسترس نیستند'}
562
  </div>
563
  `;
564
  return;
@@ -571,21 +590,21 @@ async function analyzeGlobalSentiment() {
571
 
572
  resultDiv.innerHTML = `
573
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
574
- <h4 style="margin-bottom: 15px;">احساسات کلی بازار</h4>
575
  <div style="display: grid; gap: 10px;">
576
  <div style="text-align: center; padding: 20px;">
577
  <div style="font-size: 48px; margin-bottom: 10px;">${sentimentEmoji}</div>
578
  <div style="font-size: 24px; font-weight: 700; color: ${sentimentColor}; margin-bottom: 5px;">
579
- ${sentiment === 'bullish' ? 'صعودی' : sentiment === 'bearish' ? 'نزولی' : 'خنثی'}
580
  </div>
581
  <div style="color: var(--text-secondary);">
582
- اعتماد: ${(confidence * 100).toFixed(1)}%
583
  </div>
584
  </div>
585
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
586
- <strong>جزئیات:</strong>
587
  <div style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
588
- این تحلیل بر اساس مدل‌های AI انجام شده است.
589
  </div>
590
  </div>
591
  </div>
@@ -593,8 +612,8 @@ async function analyzeGlobalSentiment() {
593
  `;
594
  } catch (error) {
595
  console.error('Global sentiment analysis error:', error);
596
- resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
597
- showError('خطا در تحلیل احساسات بازار');
598
  }
599
  }
600
 
@@ -604,12 +623,12 @@ async function analyzeAssetSentiment() {
604
  const text = document.getElementById('asset-sentiment-text').value.trim();
605
 
606
  if (!symbol) {
607
- showError('لطفاً نماد ارز را وارد کنید');
608
  return;
609
  }
610
 
611
  const resultDiv = document.getElementById('asset-sentiment-result');
612
- resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
613
 
614
  try {
615
  // Use provided text or default text with symbol
@@ -626,7 +645,7 @@ async function analyzeAssetSentiment() {
626
  if (!data.available) {
627
  resultDiv.innerHTML = `
628
  <div class="alert alert-warning">
629
- <strong>⚠️ مدل‌ها در دسترس نیستند:</strong> ${data.error || 'مدل‌های AI در حال حاضر در دسترس نیستند'}
630
  </div>
631
  `;
632
  return;
@@ -639,23 +658,23 @@ async function analyzeAssetSentiment() {
639
 
640
  resultDiv.innerHTML = `
641
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
642
- <h4 style="margin-bottom: 15px;">نتیجه تحلیل احساسات ${symbol}</h4>
643
  <div style="display: grid; gap: 10px;">
644
  <div>
645
- <strong>احساسات:</strong>
646
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
647
- ${sentimentEmoji} ${sentiment === 'bullish' ? 'صعودی' : sentiment === 'bearish' ? 'نزولی' : 'خنثی'}
648
  </span>
649
  </div>
650
  <div>
651
- <strong>اعتماد:</strong>
652
  <span style="color: var(--primary); font-weight: 600;">
653
  ${(confidence * 100).toFixed(2)}%
654
  </span>
655
  </div>
656
  ${text ? `
657
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
658
- <strong>متن تحلیل شده:</strong>
659
  <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
660
  "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
661
  </div>
@@ -666,8 +685,8 @@ async function analyzeAssetSentiment() {
666
  `;
667
  } catch (error) {
668
  console.error('Asset sentiment analysis error:', error);
669
- resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
670
- showError('خطا در تحلیل احساسات ارز');
671
  }
672
  }
673
 
@@ -677,12 +696,12 @@ async function analyzeNewsSentiment() {
677
  const content = document.getElementById('news-content').value.trim();
678
 
679
  if (!title && !content) {
680
- showError('لطفاً عنوان یا محتوای خبر را وارد کنید');
681
  return;
682
  }
683
 
684
  const resultDiv = document.getElementById('news-sentiment-result');
685
- resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
686
 
687
  try {
688
  const response = await fetch('/api/news/analyze', {
@@ -696,7 +715,7 @@ async function analyzeNewsSentiment() {
696
  if (!data.available) {
697
  resultDiv.innerHTML = `
698
  <div class="alert alert-warning">
699
- <strong>⚠️ مدل‌ها در دسترس نیستند:</strong> ${data.news?.error || data.error || 'مدل‌های AI در حال حاضر در دسترس نیستند'}
700
  </div>
701
  `;
702
  return;
@@ -712,21 +731,21 @@ async function analyzeNewsSentiment() {
712
 
713
  resultDiv.innerHTML = `
714
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
715
- <h4 style="margin-bottom: 15px;">نتیجه تحلیل خبر</h4>
716
  <div style="display: grid; gap: 10px;">
717
  <div>
718
- <strong>عنوان:</strong>
719
- <span style="color: var(--text-primary);">${title || 'بدون عنوان'}</span>
720
  </div>
721
  <div>
722
- <strong>احساسات:</strong>
723
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
724
- ${sentimentEmoji} ${sentiment === 'bullish' || sentiment === 'positive' ? 'مثبت' :
725
- sentiment === 'bearish' || sentiment === 'negative' ? 'منفی' : 'خنثی'}
726
  </span>
727
  </div>
728
  <div>
729
- <strong>اعتماد:</strong>
730
  <span style="color: var(--primary); font-weight: 600;">
731
  ${(confidence * 100).toFixed(2)}%
732
  </span>
@@ -736,8 +755,8 @@ async function analyzeNewsSentiment() {
736
  `;
737
  } catch (error) {
738
  console.error('News sentiment analysis error:', error);
739
- resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
740
- showError('خطا در تحلیل خبر');
741
  }
742
  }
743
 
@@ -748,12 +767,12 @@ async function analyzeSentiment() {
748
  const modelKey = document.getElementById('sentiment-model').value;
749
 
750
  if (!text.trim()) {
751
- showError('لطفاً متنی وارد کنید');
752
  return;
753
  }
754
 
755
  const resultDiv = document.getElementById('sentiment-result');
756
- resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
757
 
758
  try {
759
  let response;
@@ -770,7 +789,7 @@ async function analyzeSentiment() {
770
  if (!data.available) {
771
  resultDiv.innerHTML = `
772
  <div class="alert alert-warning">
773
- <strong>⚠️ مدل‌ها در دسترس نیستند:</strong> ${data.error || 'مدل‌های AI در حال حاضر در دسترس نیستند'}
774
  </div>
775
  `;
776
  return;
@@ -788,27 +807,27 @@ async function analyzeSentiment() {
788
 
789
  resultDiv.innerHTML = `
790
  <div class="alert alert-success" style="margin-top: 20px; border-left: 4px solid ${sentimentColor};">
791
- <h4 style="margin-bottom: 15px;">نتیجه تحلیل احساسات</h4>
792
  <div style="display: grid; gap: 10px;">
793
  <div>
794
- <strong>احساسات:</strong>
795
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
796
- ${sentimentEmoji} ${label === 'bullish' || label === 'positive' ? 'صعودی/مثبت' :
797
- label === 'bearish' || label === 'negative' ? 'نزولی/منفی' : 'خنثی'}
798
  </span>
799
  </div>
800
  <div>
801
- <strong>اعتماد:</strong>
802
  <span style="color: var(--primary); font-weight: 600;">
803
  ${(confidence * 100).toFixed(2)}%
804
  </span>
805
  </div>
806
  <div>
807
- <strong>نوع تحلیل:</strong>
808
  <span style="color: var(--text-secondary);">${mode}</span>
809
  </div>
810
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
811
- <strong>متن تحلیل شده:</strong>
812
  <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
813
  "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
814
  </div>
@@ -831,8 +850,8 @@ async function analyzeSentiment() {
831
 
832
  } catch (error) {
833
  console.error('Sentiment analysis error:', error);
834
- resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
835
- showError('خطا در تحلیل احساسات');
836
  }
837
  }
838
 
@@ -856,7 +875,7 @@ function loadSentimentHistory() {
856
  const historyDiv = document.getElementById('sentiment-history');
857
 
858
  if (history.length === 0) {
859
- historyDiv.innerHTML = '<div class="alert alert-warning">هیچ تاریخچه‌ای وجود ندارد</div>';
860
  return;
861
  }
862
 
@@ -869,11 +888,11 @@ function loadSentimentHistory() {
869
  <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; border-left: 3px solid var(--primary);">
870
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 5px;">
871
  <span style="font-weight: 600;">${sentimentEmoji} ${item.label}</span>
872
- <span style="font-size: 11px; color: var(--text-secondary);">${new Date(item.timestamp).toLocaleString('fa-IR')}</span>
873
  </div>
874
  <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">${item.text}</div>
875
  <div style="font-size: 11px; color: var(--text-secondary);">
876
- اعتماد: ${(item.confidence * 100).toFixed(0)}% | مدل: ${item.model}
877
  </div>
878
  </div>
879
  `;
@@ -891,7 +910,7 @@ async function loadNews() {
891
  // Try /api/news/latest first, fallback to /api/news
892
  let response;
893
  try {
894
- response = await fetch('/api/news/latest');
895
  } catch {
896
  response = await fetch('/api/news?limit=20');
897
  }
@@ -903,46 +922,149 @@ async function loadNews() {
903
  if (newsItems.length > 0) {
904
  const newsDiv = document.getElementById('news-list');
905
  newsDiv.innerHTML = `
906
- <div style="display: grid; gap: 15px;">
907
- ${newsItems.map(item => {
908
- const sentiment = item.sentiment_label || item.sentiment;
909
- const sentimentColor = sentiment === 'positive' || sentiment === 'POSITIVE' ? 'var(--success)' :
910
- sentiment === 'negative' || sentiment === 'NEGATIVE' ? 'var(--danger)' :
911
- 'var(--text-secondary)';
912
- const sentimentEmoji = sentiment === 'positive' || sentiment === 'POSITIVE' ? '📈' :
913
- sentiment === 'negative' || sentiment === 'NEGATIVE' ? '📉' : '➡️';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
 
915
  return `
916
- <div style="padding: 20px; background: rgba(31, 41, 55, 0.6); border-radius: 12px; border-left: 4px solid ${sentimentColor};">
917
- <h4 style="margin-bottom: 10px; color: var(--text-primary);">${item.title || 'بدون عنوان'}</h4>
918
- ${item.content || item.description ? `<p style="color: var(--text-secondary); margin-bottom: 10px; line-height: 1.6;">${(item.content || item.description).substring(0, 200)}...</p>` : ''}
919
- <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
920
- <div style="display: flex; gap: 15px; align-items: center;">
921
- <span style="font-size: 12px; color: var(--text-secondary);">
922
- ${item.source || 'Unknown'}
 
 
 
 
 
923
  </span>
924
- ${sentiment ? `<span style="font-size: 12px; color: ${sentimentColor};">
925
- ${sentimentEmoji} ${sentiment}
926
- ${item.sentiment_confidence ? ` (${(item.sentiment_confidence * 100).toFixed(0)}%)` : ''}
927
- </span>` : ''}
928
- ${item.published_date || item.published_at ? `<span style="font-size: 12px; color: var(--text-secondary);">
929
- ${new Date(item.published_date || item.published_at).toLocaleDateString('fa-IR')}
930
- </span>` : ''}
931
  </div>
932
- ${item.url ? `<a href="${item.url}" target="_blank" rel="noopener noreferrer" style="color: var(--primary); text-decoration: none; font-weight: 600;">مطالعه بیشتر →</a>` : ''}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933
  </div>
934
  </div>
935
  `;
936
  }).join('')}
937
  </div>
 
 
 
 
 
 
938
  `;
939
  } else {
940
- document.getElementById('news-list').innerHTML = '<div class="alert alert-warning">هیچ خبری یافت نشد</div>';
 
 
 
 
 
 
 
 
941
  }
942
  } catch (error) {
943
  console.error('Error loading news:', error);
944
- showError('خطا در بارگذاری اخبار');
945
- document.getElementById('news-list').innerHTML = '<div class="alert alert-error">خطا در بارگذاری اخبار</div>';
 
 
 
 
 
 
 
 
946
  }
947
  }
948
 
@@ -968,29 +1090,29 @@ async function loadProviders() {
968
  <thead>
969
  <tr>
970
  <th>ID</th>
971
- <th>نام</th>
972
- <th>دسته</th>
973
- <th>نوع</th>
974
- <th>وضعیت</th>
975
- <th>جزئیات</th>
976
  </tr>
977
  </thead>
978
  <tbody>
979
  ${providers.map(provider => {
980
  const status = provider.status || 'unknown';
981
  const statusConfig = {
982
- 'VALID': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
983
- 'validated': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
984
- 'available': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ در دسترس' },
985
- 'online': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ آنلاین' },
986
- 'CONDITIONALLY_AVAILABLE': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ شرطی' },
987
- 'INVALID': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ نامعتبر' },
988
- 'unvalidated': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ نامعتبر' },
989
- 'not_loaded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ بارگذاری نشده' },
990
- 'offline': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ آفلاین' },
991
- 'degraded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ تخریب شده' }
992
  };
993
- const statusInfo = statusConfig[status] || { color: 'var(--text-secondary)', bg: 'rgba(156, 163, 175, 0.2)', text: '❓ نامشخص' };
994
 
995
  return `
996
  <tr>
@@ -1015,11 +1137,11 @@ async function loadProviders() {
1015
  </table>
1016
  </div>
1017
  <div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
1018
- <strong>کل Providerها:</strong> ${providersData.total || providers.length}
1019
  </div>
1020
  `;
1021
  } else {
1022
- providersDiv.innerHTML = '<div class="alert alert-warning">هیچ Providerی یافت نشد</div>';
1023
  }
1024
  }
1025
 
@@ -1061,10 +1183,10 @@ async function loadProviders() {
1061
 
1062
  } catch (error) {
1063
  console.error('Error loading providers:', error);
1064
- showError('خطا در بارگذاری Providerها');
1065
  const providersDiv = document.getElementById('providers-list');
1066
  if (providersDiv) {
1067
- providersDiv.innerHTML = '<div class="alert alert-error">خطا در بارگذاری Providerها</div>';
1068
  }
1069
  }
1070
  }
@@ -1073,12 +1195,12 @@ async function loadProviders() {
1073
  async function searchResources() {
1074
  const query = document.getElementById('search-resources').value;
1075
  if (!query.trim()) {
1076
- showError('لطفاً عبارتی برای جستجو وارد کنید');
1077
  return;
1078
  }
1079
 
1080
  const resultsDiv = document.getElementById('search-results');
1081
- resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال جستجو...</div>';
1082
 
1083
  try {
1084
  const response = await fetch(`/api/resources/search?q=${encodeURIComponent(query)}`);
@@ -1088,7 +1210,7 @@ async function searchResources() {
1088
  resultsDiv.innerHTML = `
1089
  <div style="margin-top: 15px;">
1090
  <div style="margin-bottom: 10px; color: var(--text-secondary);">
1091
- ${data.count || data.resources.length} نتیجه یافت شد
1092
  </div>
1093
  <div style="display: grid; gap: 10px;">
1094
  ${data.resources.map(resource => `
@@ -1097,7 +1219,7 @@ async function searchResources() {
1097
  <div>
1098
  <strong style="font-size: 16px;">${resource.name || 'Unknown'}</strong>
1099
  <div style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">
1100
- دسته: ${resource.category || 'N/A'}
1101
  </div>
1102
  ${resource.base_url ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 3px; font-family: monospace;">
1103
  ${resource.base_url}
@@ -1105,7 +1227,7 @@ async function searchResources() {
1105
  </div>
1106
  ${resource.free !== undefined ? `
1107
  <span style="padding: 5px 10px; border-radius: 5px; background: ${resource.free ? 'rgba(16, 185, 129, 0.2)' : 'rgba(245, 158, 11, 0.2)'}; color: ${resource.free ? 'var(--success)' : 'var(--warning)'}; font-size: 12px;">
1108
- ${resource.free ? '🆓 رایگان' : '💰 پولی'}
1109
  </span>
1110
  ` : ''}
1111
  </div>
@@ -1115,12 +1237,12 @@ async function searchResources() {
1115
  </div>
1116
  `;
1117
  } else {
1118
- resultsDiv.innerHTML = '<div class="alert alert-warning" style="margin-top: 15px;">نتیجه‌ای یافت نشد</div>';
1119
  }
1120
  } catch (error) {
1121
  console.error('Search error:', error);
1122
- resultsDiv.innerHTML = '<div class="alert alert-error" style="margin-top: 15px;">خطا در جستجو</div>';
1123
- showError('خطا در جستجو');
1124
  }
1125
  }
1126
 
@@ -1139,20 +1261,20 @@ async function loadDiagnostics() {
1139
 
1140
  statusDiv.innerHTML = `
1141
  <div class="alert ${healthClass}">
1142
- <h4 style="margin-bottom: 10px;">وضعیت سیستم</h4>
1143
  <div style="display: grid; gap: 5px;">
1144
- <div><strong>وضعیت کلی:</strong> ${health}</div>
1145
- <div><strong>APIهای کل:</strong> ${statusData.total_apis || 0}</div>
1146
- <div><strong>آنلاین:</strong> ${statusData.online || 0}</div>
1147
- <div><strong>تخریب شده:</strong> ${statusData.degraded || 0}</div>
1148
- <div><strong>آفلاین:</strong> ${statusData.offline || 0}</div>
1149
- <div><strong>میانگین زمان پاسخ:</strong> ${statusData.avg_response_time_ms || 0}ms</div>
1150
- ${statusData.last_update ? `<div><strong>آخرین به‌روزرسانی:</strong> ${new Date(statusData.last_update).toLocaleString('fa-IR')}</div>` : ''}
1151
  </div>
1152
  </div>
1153
  `;
1154
  } catch (statusError) {
1155
- document.getElementById('diagnostics-status').innerHTML = '<div class="alert alert-error">خطا در بارگذاری وضعیت سیستم</div>';
1156
  }
1157
 
1158
  // Load error logs
@@ -1169,25 +1291,25 @@ async function loadDiagnostics() {
1169
  ${errors.slice(0, 10).map(error => `
1170
  <div style="padding: 15px; background: rgba(239, 68, 68, 0.1); border-left: 4px solid var(--danger); border-radius: 5px;">
1171
  <div style="font-weight: 600; color: var(--danger); margin-bottom: 5px;">
1172
- ${error.message || error.error_message || error.type || 'خطا'}
1173
  </div>
1174
- ${error.error_type ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">نوع: ${error.error_type}</div>` : ''}
1175
  ${error.provider ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">Provider: ${error.provider}</div>` : ''}
1176
  <div style="font-size: 11px; color: var(--text-secondary); margin-top: 5px;">
1177
- ${error.timestamp ? new Date(error.timestamp).toLocaleString('fa-IR') : ''}
1178
  </div>
1179
  </div>
1180
  `).join('')}
1181
  </div>
1182
  ${errors.length > 10 ? `<div style="margin-top: 10px; text-align: center; color: var(--text-secondary); font-size: 12px;">
1183
- نمایش ${Math.min(10, errors.length)} از ${errors.length} خطا
1184
  </div>` : ''}
1185
  `;
1186
  } else {
1187
- errorsDiv.innerHTML = '<div class="alert alert-success">هیچ خطایی یافت نشد ✅</div>';
1188
  }
1189
  } catch (errorsError) {
1190
- document.getElementById('error-logs').innerHTML = '<div class="alert alert-warning">خطا در بارگذاری گزارش خطاها</div>';
1191
  }
1192
 
1193
  // Load recent logs
@@ -1214,7 +1336,7 @@ async function loadDiagnostics() {
1214
  ${level}
1215
  </div>
1216
  <div style="font-size: 11px; color: var(--text-secondary);">
1217
- ${log.timestamp ? new Date(log.timestamp).toLocaleString('fa-IR') : ''}
1218
  </div>
1219
  </div>
1220
  <div style="font-size: 13px; color: var(--text-primary);">
@@ -1227,14 +1349,14 @@ async function loadDiagnostics() {
1227
  </div>
1228
  `;
1229
  } else {
1230
- logsDiv.innerHTML = '<div class="alert alert-warning">هیچ لاگی یافت نشد</div>';
1231
  }
1232
  } catch (logsError) {
1233
- document.getElementById('recent-logs').innerHTML = '<div class="alert alert-warning">خطا در بارگذاری لاگ‌ها</div>';
1234
  }
1235
  } catch (error) {
1236
  console.error('Error loading diagnostics:', error);
1237
- showError('خطا در بارگذاری خطایابی');
1238
  }
1239
  }
1240
 
@@ -1245,13 +1367,13 @@ async function runDiagnostics() {
1245
  const data = await response.json();
1246
 
1247
  if (data.success) {
1248
- showSuccess('تشخیص با موفقیت اجرا شد');
1249
  setTimeout(loadDiagnostics, 1000);
1250
  } else {
1251
- showError(data.error || 'خطا در اجرای تشخیص');
1252
  }
1253
  } catch (error) {
1254
- showError('خطا در اجرای تشخیص: ' + error.message);
1255
  }
1256
  }
1257
 
@@ -1262,12 +1384,12 @@ async function testAPI() {
1262
  const bodyText = document.getElementById('api-body').value;
1263
 
1264
  if (!endpoint) {
1265
- showError('لطفاً یک endpoint انتخاب کنید');
1266
  return;
1267
  }
1268
 
1269
  const resultDiv = document.getElementById('api-result');
1270
- resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال ارسال درخواست...</div>';
1271
 
1272
  try {
1273
  const options = { method };
@@ -1279,8 +1401,8 @@ async function testAPI() {
1279
  body = JSON.parse(bodyText);
1280
  options.headers = { 'Content-Type': 'application/json' };
1281
  } catch (e) {
1282
- showError('JSON نامعتبر در body');
1283
- resultDiv.innerHTML = '<div class="alert alert-error">خطا در پارس JSON</div>';
1284
  return;
1285
  }
1286
  }
@@ -1310,15 +1432,15 @@ async function testAPI() {
1310
  <div class="alert ${statusClass}" style="margin-bottom: 15px;">
1311
  <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
1312
  <div>
1313
- <strong>${statusEmoji} وضعیت:</strong> ${response.status} ${response.statusText}
1314
  </div>
1315
  <div style="font-size: 12px; color: var(--text-secondary);">
1316
- زمان پاسخ: ${responseTime}ms
1317
  </div>
1318
  </div>
1319
  </div>
1320
  <div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px;">
1321
- <h4 style="margin-bottom: 10px;">پاسخ:</h4>
1322
  <pre style="background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 5px; overflow-x: auto; margin-top: 10px; font-size: 12px; max-height: 500px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
1323
  </div>
1324
  <div style="margin-top: 10px; padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 5px; font-size: 12px; color: var(--text-secondary);">
@@ -1329,11 +1451,11 @@ async function testAPI() {
1329
  } catch (error) {
1330
  resultDiv.innerHTML = `
1331
  <div class="alert alert-error" style="margin-top: 20px;">
1332
- <h4>خطا:</h4>
1333
  <p>${error.message}</p>
1334
  </div>
1335
  `;
1336
- showError('خطا در تست API: ' + error.message);
1337
  }
1338
  }
1339
 
@@ -1513,6 +1635,18 @@ async function loadAPIRegistry() {
1513
 
1514
  if (!data.ok) {
1515
  console.warn('API registry not available:', data.error);
 
 
 
 
 
 
 
 
 
 
 
 
1516
  return;
1517
  }
1518
 
@@ -1520,17 +1654,85 @@ async function loadAPIRegistry() {
1520
  if (registryContainer) {
1521
  const metadata = data.metadata || {};
1522
  const categories = data.categories || [];
 
1523
 
1524
  registryContainer.innerHTML = `
1525
- <div class="card">
1526
- <h3>API Registry: ${metadata.name || 'Unknown'}</h3>
1527
- <p>Version: ${metadata.version || 'N/A'}</p>
1528
- <p>Description: ${metadata.description || 'N/A'}</p>
1529
- <h4>Categories:</h4>
1530
- <ul>
1531
- ${categories.map(cat => `<li>${cat}</li>`).join('')}
1532
- </ul>
1533
- <p>Total Files: ${data.total_raw_files || 0}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1534
  </div>
1535
  `;
1536
  }
@@ -1539,14 +1741,26 @@ async function loadAPIRegistry() {
1539
  const metadataContainer = document.getElementById('api-registry-metadata');
1540
  if (metadataContainer) {
1541
  metadataContainer.innerHTML = `
1542
- <div class="card">
1543
- <h4>Metadata</h4>
1544
- <pre>${JSON.stringify(metadata, null, 2)}</pre>
1545
  </div>
1546
  `;
1547
  }
1548
  } catch (error) {
1549
  console.error('Error loading API registry:', error);
 
 
 
 
 
 
 
 
 
 
 
 
1550
  }
1551
  }
1552
 
 
130
  ];
131
 
132
  // Clear existing options except first one
133
+ endpointSelect.innerHTML = '<option value="">Select Endpoint...</option>';
134
  endpoints.forEach(ep => {
135
  const option = document.createElement('option');
136
  option.value = ep.value;
 
148
  const statusBadge = document.getElementById('api-status');
149
  if (data.status === 'healthy') {
150
  statusBadge.className = 'status-badge';
151
+ statusBadge.innerHTML = '<span class="status-dot"></span><span>✅ System Active</span>';
152
  } else {
153
  statusBadge.className = 'status-badge error';
154
+ statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ Error</span>';
155
  }
156
  } catch (error) {
157
  const statusBadge = document.getElementById('api-status');
158
  statusBadge.className = 'status-badge error';
159
+ statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ Connection Failed</span>';
160
  }
161
  }
162
 
 
188
 
189
  systemStatusDiv.innerHTML = `
190
  <div class="alert ${healthClass}">
191
+ <strong>System Status:</strong> ${healthStatus}<br>
192
+ <strong>Online APIs:</strong> ${statusData.online || 0}<br>
193
+ <strong>Degraded APIs:</strong> ${statusData.degraded || 0}<br>
194
+ <strong>Offline APIs:</strong> ${statusData.offline || 0}<br>
195
+ <strong>Avg Response Time:</strong> ${statusData.avg_response_time_ms || 0}ms<br>
196
+ <strong>Last Update:</strong> ${new Date(statusData.last_update || Date.now()).toLocaleString('en-US')}
197
  </div>
198
  `;
199
  } catch (statusError) {
 
207
  }
208
  } catch (error) {
209
  console.error('Error loading dashboard:', error);
210
+ showError('Error loading dashboard');
211
  }
212
  }
213
 
 
225
  data: {
226
  labels: Object.keys(categories),
227
  datasets: [{
228
+ label: 'Total Resources',
229
  data: Object.values(categories),
230
  backgroundColor: 'rgba(102, 126, 234, 0.6)',
231
  borderColor: 'rgba(102, 126, 234, 1)',
 
258
  <thead>
259
  <tr>
260
  <th>#</th>
261
+ <th>Name</th>
262
+ <th>Price (USD)</th>
263
+ <th>24h Change</th>
264
+ <th>24h Volume</th>
265
+ <th>Market Cap</th>
266
  </tr>
267
  </thead>
268
  <tbody>
 
285
  </table>
286
  </div>
287
  ${data.total_market_cap ? `<div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
288
+ <strong>Total Market Cap:</strong> $${formatNumber(data.total_market_cap)} |
289
  <strong>BTC Dominance:</strong> ${(data.btc_dominance || 0).toFixed(2)}%
290
  </div>` : ''}
291
  `;
292
  } else {
293
+ document.getElementById('market-data').innerHTML = '<div class="alert alert-warning">No data found</div>';
294
  }
295
 
296
  // Load trending
 
308
  <span style="font-size: 18px; font-weight: 800; color: var(--primary);">#${index + 1}</span>
309
  <div>
310
  <strong>${coin.symbol || coin.id}</strong> - ${coin.name || 'Unknown'}
311
+ ${coin.market_cap_rank ? `<div style="font-size: 12px; color: var(--text-secondary);">Market Cap Rank: ${coin.market_cap_rank}</div>` : ''}
312
  </div>
313
  </div>
314
  <div style="font-size: 20px; font-weight: 700; color: var(--success);">${coin.score ? coin.score.toFixed(2) : 'N/A'}</div>
 
317
  </div>
318
  `;
319
  } else {
320
+ document.getElementById('trending-coins').innerHTML = '<div class="alert alert-warning">No data found</div>';
321
  }
322
  } catch (trendingError) {
323
  console.warn('Trending endpoint error:', trendingError);
324
+ document.getElementById('trending-coins').innerHTML = '<div class="alert alert-error">Error loading trending coins</div>';
325
  }
326
 
327
  // Load Fear & Greed
 
350
  ${fgLabel}
351
  </div>
352
  <div style="font-size: 14px; color: var(--text-secondary);">
353
+ Market Fear & Greed Index
354
  </div>
355
  ${sentimentData.timestamp ? `<div style="font-size: 12px; color: var(--text-secondary); margin-top: 10px;">
356
+ Last Update: ${new Date(sentimentData.timestamp).toLocaleString('en-US')}
357
  </div>` : ''}
358
  </div>
359
  `;
360
  } else {
361
+ document.getElementById('fear-greed').innerHTML = '<div class="alert alert-warning">No data found</div>';
362
  }
363
  } catch (sentimentError) {
364
  console.warn('Sentiment endpoint error:', sentimentError);
365
+ document.getElementById('fear-greed').innerHTML = '<div class="alert alert-error">Error loading Fear & Greed Index</div>';
366
  }
367
  } catch (error) {
368
  console.error('Error loading market data:', error);
369
+ showError('Error loading market data');
370
  }
371
  }
372
 
 
406
  <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">
407
  ${model.task || model.category || 'N/A'}
408
  </div>
409
+ ${model.category ? `<div style="font-size: 11px; color: var(--text-secondary);">Category: ${model.category}</div>` : ''}
410
  ${model.requires_auth !== undefined ? `<div style="font-size: 11px; color: var(--text-secondary);">
411
+ ${model.requires_auth ? '🔐 Requires Authentication' : '🔓 No Auth Required'}
412
  </div>` : ''}
413
  </div>
414
  <span style="background: ${statusBg}; color: ${statusColor}; padding: 5px 10px; border-radius: 5px; font-size: 12px; font-weight: 600;">
415
+ ${isAvailable ? '✅ Available' : '❌ Unavailable'}
416
  </span>
417
  </div>
418
  ${model.key ? `<div style="margin-top: 10px; font-size: 11px; color: var(--text-secondary); font-family: monospace;">
 
424
  </div>
425
  `;
426
  } else {
427
+ document.getElementById('models-list').innerHTML = '<div class="alert alert-warning">No models found</div>';
428
  }
429
 
430
  // Load models status
 
433
  const statusData = await statusRes.json();
434
 
435
  const statusDiv = document.getElementById('models-status');
436
+ if (statusDiv) {
437
+ // Use honest status from backend
438
+ const status = statusData.status || 'unknown';
439
+ const statusMessage = statusData.status_message || 'Unknown status';
440
+ const hfMode = statusData.hf_mode || 'unknown';
441
+ const modelsLoaded = statusData.models_loaded || statusData.pipelines_loaded || 0;
442
+ const modelsFailed = statusData.models_failed || 0;
443
+
444
+ // Determine status class based on honest status
445
+ let statusClass = 'alert-warning';
446
+ if (status === 'ok') statusClass = 'alert-success';
447
+ else if (status === 'disabled' || status === 'transformers_unavailable') statusClass = 'alert-error';
448
+ else if (status === 'partial') statusClass = 'alert-warning';
449
+
450
+ statusDiv.innerHTML = `
451
+ <div class="alert ${statusClass}">
452
+ <strong>Status:</strong> ${statusMessage}<br>
453
+ <strong>HF Mode:</strong> ${hfMode}<br>
454
+ <strong>Models Loaded:</strong> ${modelsLoaded}<br>
455
+ <strong>Models Failed:</strong> ${modelsFailed}<br>
456
+ ${statusData.transformers_available !== undefined ? `<strong>Transformers Available:</strong> ${statusData.transformers_available ? '✅ Yes' : '❌ No'}<br>` : ''}
457
+ ${statusData.initialized !== undefined ? `<strong>Initialized:</strong> ${statusData.initialized ? '✅ Yes' : '❌ No'}<br>` : ''}
458
+ ${hfMode === 'off' ? `<div style="margin-top: 10px; padding: 10px; background: rgba(239, 68, 68, 0.1); border-radius: 5px; font-size: 12px;">
459
+ <strong>Note:</strong> HF models are disabled (HF_MODE=off). To enable them, set HF_MODE=public or HF_MODE=auth in the environment.
460
+ </div>` : ''}
461
+ ${hfMode !== 'off' && modelsLoaded === 0 && modelsFailed > 0 ? `<div style="margin-top: 10px; padding: 10px; background: rgba(245, 158, 11, 0.1); border-radius: 5px; font-size: 12px;">
462
+ <strong>Warning:</strong> No models could be loaded. ${modelsFailed} model(s) failed. Check model IDs or HF access.
463
+ </div>` : ''}
464
+ </div>
465
+ `;
466
+ }
467
  } catch (statusError) {
468
  console.warn('Models status endpoint error:', statusError);
469
  }
 
479
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
480
  <div style="padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
481
  <div style="font-size: 28px; font-weight: 800; color: var(--primary);">${statsData.statistics.total_analyses || 0}</div>
482
+ <div style="font-size: 14px; color: var(--text-secondary);">Total Analyses</div>
483
  </div>
484
  <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px;">
485
  <div style="font-size: 28px; font-weight: 800; color: var(--success);">${statsData.statistics.unique_symbols || 0}</div>
486
+ <div style="font-size: 14px; color: var(--text-secondary);">Unique Symbols</div>
487
  </div>
488
  ${statsData.statistics.most_used_model ? `
489
  <div style="padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 10px;">
490
  <div style="font-size: 18px; font-weight: 800; color: var(--warning);">${statsData.statistics.most_used_model}</div>
491
+ <div style="font-size: 14px; color: var(--text-secondary);">Most Used Model</div>
492
  </div>
493
  ` : ''}
494
  </div>
 
499
  }
500
  } catch (error) {
501
  console.error('Error loading models:', error);
502
+ showError('Error loading models');
503
  }
504
  }
505
 
 
510
  const data = await response.json();
511
 
512
  if (data.success) {
513
+ showSuccess('Models loaded successfully');
514
  loadModels();
515
  } else {
516
+ showError(data.error || 'Error loading models');
517
  }
518
  } catch (error) {
519
+ showError('Error loading models: ' + error.message);
520
  }
521
  }
522
 
 
528
 
529
  const models = data.models || data || [];
530
  const select = document.getElementById('sentiment-model');
531
+ select.innerHTML = '<option value="">Select Model...</option>';
532
 
533
  models.filter(m => {
534
  const status = m.status || 'unknown';
 
546
  if (select.options.length === 1) {
547
  const option = document.createElement('option');
548
  option.value = '';
549
+ option.textContent = 'No models available';
550
  option.disabled = true;
551
  select.appendChild(option);
552
  }
553
  } catch (error) {
554
  console.error('Error loading sentiment models:', error);
555
  const select = document.getElementById('sentiment-model');
556
+ select.innerHTML = '<option value="">Error loading models</option>';
557
  }
558
  }
559
 
560
  // Analyze Global Market Sentiment
561
  async function analyzeGlobalSentiment() {
562
  const resultDiv = document.getElementById('global-sentiment-result');
563
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing market sentiment...</div>';
564
 
565
  try {
566
  // Use market text analysis with sample market-related text
 
577
  if (!data.available) {
578
  resultDiv.innerHTML = `
579
  <div class="alert alert-warning">
580
+ <strong>⚠️ Models Not Available:</strong> ${data.error || 'AI models are currently unavailable'}
581
  </div>
582
  `;
583
  return;
 
590
 
591
  resultDiv.innerHTML = `
592
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
593
+ <h4 style="margin-bottom: 15px;">Global Market Sentiment</h4>
594
  <div style="display: grid; gap: 10px;">
595
  <div style="text-align: center; padding: 20px;">
596
  <div style="font-size: 48px; margin-bottom: 10px;">${sentimentEmoji}</div>
597
  <div style="font-size: 24px; font-weight: 700; color: ${sentimentColor}; margin-bottom: 5px;">
598
+ ${sentiment === 'bullish' ? 'Bullish' : sentiment === 'bearish' ? 'Bearish' : 'Neutral'}
599
  </div>
600
  <div style="color: var(--text-secondary);">
601
+ Confidence: ${(confidence * 100).toFixed(1)}%
602
  </div>
603
  </div>
604
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
605
+ <strong>Details:</strong>
606
  <div style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
607
+ This analysis is based on AI models.
608
  </div>
609
  </div>
610
  </div>
 
612
  `;
613
  } catch (error) {
614
  console.error('Global sentiment analysis error:', error);
615
+ resultDiv.innerHTML = `<div class="alert alert-error">Analysis Error: ${error.message}</div>`;
616
+ showError('Error analyzing market sentiment');
617
  }
618
  }
619
 
 
623
  const text = document.getElementById('asset-sentiment-text').value.trim();
624
 
625
  if (!symbol) {
626
+ showError('Please enter a cryptocurrency symbol');
627
  return;
628
  }
629
 
630
  const resultDiv = document.getElementById('asset-sentiment-result');
631
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
632
 
633
  try {
634
  // Use provided text or default text with symbol
 
645
  if (!data.available) {
646
  resultDiv.innerHTML = `
647
  <div class="alert alert-warning">
648
+ <strong>⚠️ Models Not Available:</strong> ${data.error || 'AI models are currently unavailable'}
649
  </div>
650
  `;
651
  return;
 
658
 
659
  resultDiv.innerHTML = `
660
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
661
+ <h4 style="margin-bottom: 15px;">Sentiment Analysis Result for ${symbol}</h4>
662
  <div style="display: grid; gap: 10px;">
663
  <div>
664
+ <strong>Sentiment:</strong>
665
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
666
+ ${sentimentEmoji} ${sentiment === 'bullish' ? 'Bullish' : sentiment === 'bearish' ? 'Bearish' : 'Neutral'}
667
  </span>
668
  </div>
669
  <div>
670
+ <strong>Confidence:</strong>
671
  <span style="color: var(--primary); font-weight: 600;">
672
  ${(confidence * 100).toFixed(2)}%
673
  </span>
674
  </div>
675
  ${text ? `
676
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
677
+ <strong>Analyzed Text:</strong>
678
  <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
679
  "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
680
  </div>
 
685
  `;
686
  } catch (error) {
687
  console.error('Asset sentiment analysis error:', error);
688
+ resultDiv.innerHTML = `<div class="alert alert-error">Analysis Error: ${error.message}</div>`;
689
+ showError('Error analyzing asset sentiment');
690
  }
691
  }
692
 
 
696
  const content = document.getElementById('news-content').value.trim();
697
 
698
  if (!title && !content) {
699
+ showError('Please enter news title or content');
700
  return;
701
  }
702
 
703
  const resultDiv = document.getElementById('news-sentiment-result');
704
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
705
 
706
  try {
707
  const response = await fetch('/api/news/analyze', {
 
715
  if (!data.available) {
716
  resultDiv.innerHTML = `
717
  <div class="alert alert-warning">
718
+ <strong>⚠️ Models Not Available:</strong> ${data.news?.error || data.error || 'AI models are currently unavailable'}
719
  </div>
720
  `;
721
  return;
 
731
 
732
  resultDiv.innerHTML = `
733
  <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
734
+ <h4 style="margin-bottom: 15px;">News Sentiment Analysis Result</h4>
735
  <div style="display: grid; gap: 10px;">
736
  <div>
737
+ <strong>Title:</strong>
738
+ <span style="color: var(--text-primary);">${title || 'No title'}</span>
739
  </div>
740
  <div>
741
+ <strong>Sentiment:</strong>
742
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
743
+ ${sentimentEmoji} ${sentiment === 'bullish' || sentiment === 'positive' ? 'Positive' :
744
+ sentiment === 'bearish' || sentiment === 'negative' ? 'Negative' : 'Neutral'}
745
  </span>
746
  </div>
747
  <div>
748
+ <strong>Confidence:</strong>
749
  <span style="color: var(--primary); font-weight: 600;">
750
  ${(confidence * 100).toFixed(2)}%
751
  </span>
 
755
  `;
756
  } catch (error) {
757
  console.error('News sentiment analysis error:', error);
758
+ resultDiv.innerHTML = `<div class="alert alert-error">Analysis Error: ${error.message}</div>`;
759
+ showError('Error analyzing news sentiment');
760
  }
761
  }
762
 
 
767
  const modelKey = document.getElementById('sentiment-model').value;
768
 
769
  if (!text.trim()) {
770
+ showError('Please enter text to analyze');
771
  return;
772
  }
773
 
774
  const resultDiv = document.getElementById('sentiment-result');
775
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
776
 
777
  try {
778
  let response;
 
789
  if (!data.available) {
790
  resultDiv.innerHTML = `
791
  <div class="alert alert-warning">
792
+ <strong>⚠️ Models Not Available:</strong> ${data.error || 'AI models are currently unavailable'}
793
  </div>
794
  `;
795
  return;
 
807
 
808
  resultDiv.innerHTML = `
809
  <div class="alert alert-success" style="margin-top: 20px; border-left: 4px solid ${sentimentColor};">
810
+ <h4 style="margin-bottom: 15px;">Sentiment Analysis Result</h4>
811
  <div style="display: grid; gap: 10px;">
812
  <div>
813
+ <strong>Sentiment:</strong>
814
  <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
815
+ ${sentimentEmoji} ${label === 'bullish' || label === 'positive' ? 'Bullish/Positive' :
816
+ label === 'bearish' || label === 'negative' ? 'Bearish/Negative' : 'Neutral'}
817
  </span>
818
  </div>
819
  <div>
820
+ <strong>Confidence:</strong>
821
  <span style="color: var(--primary); font-weight: 600;">
822
  ${(confidence * 100).toFixed(2)}%
823
  </span>
824
  </div>
825
  <div>
826
+ <strong>Analysis Type:</strong>
827
  <span style="color: var(--text-secondary);">${mode}</span>
828
  </div>
829
  <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
830
+ <strong>Analyzed Text:</strong>
831
  <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
832
  "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
833
  </div>
 
850
 
851
  } catch (error) {
852
  console.error('Sentiment analysis error:', error);
853
+ resultDiv.innerHTML = `<div class="alert alert-error">Analysis Error: ${error.message}</div>`;
854
+ showError('Error analyzing sentiment');
855
  }
856
  }
857
 
 
875
  const historyDiv = document.getElementById('sentiment-history');
876
 
877
  if (history.length === 0) {
878
+ historyDiv.innerHTML = '<div class="alert alert-warning">No history available</div>';
879
  return;
880
  }
881
 
 
888
  <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; border-left: 3px solid var(--primary);">
889
  <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 5px;">
890
  <span style="font-weight: 600;">${sentimentEmoji} ${item.label}</span>
891
+ <span style="font-size: 11px; color: var(--text-secondary);">${new Date(item.timestamp).toLocaleString('en-US')}</span>
892
  </div>
893
  <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">${item.text}</div>
894
  <div style="font-size: 11px; color: var(--text-secondary);">
895
+ Confidence: ${(item.confidence * 100).toFixed(0)}% | Model: ${item.model}
896
  </div>
897
  </div>
898
  `;
 
910
  // Try /api/news/latest first, fallback to /api/news
911
  let response;
912
  try {
913
+ response = await fetch('/api/news/latest?limit=20');
914
  } catch {
915
  response = await fetch('/api/news?limit=20');
916
  }
 
922
  if (newsItems.length > 0) {
923
  const newsDiv = document.getElementById('news-list');
924
  newsDiv.innerHTML = `
925
+ <div style="display: grid; gap: 20px;">
926
+ ${newsItems.map((item, index) => {
927
+ const sentiment = item.sentiment_label || item.sentiment || 'neutral';
928
+ const sentimentLower = sentiment.toLowerCase();
929
+ const sentimentConfidence = item.sentiment_confidence || 0;
930
+
931
+ // Determine sentiment styling
932
+ let sentimentColor, sentimentBg, sentimentEmoji, sentimentLabel;
933
+ if (sentimentLower.includes('positive') || sentimentLower.includes('bullish')) {
934
+ sentimentColor = '#10b981';
935
+ sentimentBg = 'rgba(16, 185, 129, 0.15)';
936
+ sentimentEmoji = '📈';
937
+ sentimentLabel = 'Bullish';
938
+ } else if (sentimentLower.includes('negative') || sentimentLower.includes('bearish')) {
939
+ sentimentColor = '#ef4444';
940
+ sentimentBg = 'rgba(239, 68, 68, 0.15)';
941
+ sentimentEmoji = '📉';
942
+ sentimentLabel = 'Bearish';
943
+ } else {
944
+ sentimentColor = '#6b7280';
945
+ sentimentBg = 'rgba(107, 114, 128, 0.15)';
946
+ sentimentEmoji = '➡️';
947
+ sentimentLabel = 'Neutral';
948
+ }
949
+
950
+ const publishedDate = item.published_date || item.published_at || item.analyzed_at;
951
+ const publishedTime = publishedDate ? new Date(publishedDate).toLocaleString('en-US', {
952
+ year: 'numeric',
953
+ month: 'short',
954
+ day: 'numeric',
955
+ hour: '2-digit',
956
+ minute: '2-digit'
957
+ }) : 'Unknown date';
958
+
959
+ const content = item.content || item.description || '';
960
+ const contentPreview = content.length > 250 ? content.substring(0, 250) + '...' : content;
961
 
962
  return `
963
+ <div style="padding: 24px; background: rgba(31, 41, 55, 0.6); border-radius: 16px; border-left: 5px solid ${sentimentColor}; transition: transform 0.2s, box-shadow 0.2s; cursor: pointer;"
964
+ onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 10px 25px rgba(0,0,0,0.3)'"
965
+ onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'"
966
+ onclick="${item.url ? `window.open('${item.url}', '_blank')` : ''}">
967
+ <div style="display: flex; justify-content: space-between; align-items: start; gap: 15px; margin-bottom: 12px;">
968
+ <h4 style="margin: 0; color: var(--text-primary); font-size: 18px; font-weight: 700; line-height: 1.4; flex: 1;">
969
+ ${item.title || 'No title'}
970
+ </h4>
971
+ <div style="padding: 6px 12px; background: ${sentimentBg}; border-radius: 8px; white-space: nowrap;">
972
+ <span style="font-size: 16px; margin-right: 4px;">${sentimentEmoji}</span>
973
+ <span style="font-size: 12px; font-weight: 600; color: ${sentimentColor};">
974
+ ${sentimentLabel}
975
  </span>
 
 
 
 
 
 
 
976
  </div>
977
+ </div>
978
+
979
+ ${contentPreview ? `
980
+ <p style="color: var(--text-secondary); margin-bottom: 15px; line-height: 1.7; font-size: 14px;">
981
+ ${contentPreview}
982
+ </p>
983
+ ` : ''}
984
+
985
+ <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; padding-top: 12px; border-top: 1px solid rgba(255, 255, 255, 0.1);">
986
+ <div style="display: flex; gap: 15px; align-items: center; flex-wrap: wrap;">
987
+ <div style="display: flex; align-items: center; gap: 6px;">
988
+ <span style="font-size: 12px; color: var(--text-secondary);">📰</span>
989
+ <span style="font-size: 12px; color: var(--text-secondary); font-weight: 500;">
990
+ ${item.source || 'Unknown Source'}
991
+ </span>
992
+ </div>
993
+
994
+ ${sentimentConfidence > 0 ? `
995
+ <div style="display: flex; align-items: center; gap: 6px;">
996
+ <span style="font-size: 12px; color: var(--text-secondary);">🎯</span>
997
+ <span style="font-size: 12px; color: ${sentimentColor}; font-weight: 600;">
998
+ ${(sentimentConfidence * 100).toFixed(0)}% confidence
999
+ </span>
1000
+ </div>
1001
+ ` : ''}
1002
+
1003
+ <div style="display: flex; align-items: center; gap: 6px;">
1004
+ <span style="font-size: 12px; color: var(--text-secondary);">🕒</span>
1005
+ <span style="font-size: 12px; color: var(--text-secondary);">
1006
+ ${publishedTime}
1007
+ </span>
1008
+ </div>
1009
+
1010
+ ${item.related_symbols && Array.isArray(item.related_symbols) && item.related_symbols.length > 0 ? `
1011
+ <div style="display: flex; align-items: center; gap: 6px;">
1012
+ <span style="font-size: 12px; color: var(--text-secondary);">💰</span>
1013
+ <div style="display: flex; gap: 4px; flex-wrap: wrap;">
1014
+ ${item.related_symbols.slice(0, 3).map(symbol => `
1015
+ <span style="padding: 2px 8px; background: rgba(59, 130, 246, 0.2); border-radius: 4px; font-size: 11px; color: var(--accent-blue); font-weight: 600;">
1016
+ ${symbol}
1017
+ </span>
1018
+ `).join('')}
1019
+ ${item.related_symbols.length > 3 ? `<span style="font-size: 11px; color: var(--text-secondary);">+${item.related_symbols.length - 3}</span>` : ''}
1020
+ </div>
1021
+ </div>
1022
+ ` : ''}
1023
+ </div>
1024
+
1025
+ ${item.url ? `
1026
+ <a href="${item.url}" target="_blank" rel="noopener noreferrer"
1027
+ style="padding: 8px 16px; background: var(--accent-blue); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 13px; transition: background 0.2s;"
1028
+ onmouseover="this.style.background='#2563eb'"
1029
+ onmouseout="this.style.background='var(--accent-blue)'">
1030
+ Read More →
1031
+ </a>
1032
+ ` : ''}
1033
  </div>
1034
  </div>
1035
  `;
1036
  }).join('')}
1037
  </div>
1038
+ <div style="margin-top: 20px; padding: 15px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; text-align: center;">
1039
+ <span style="font-size: 14px; color: var(--text-secondary);">
1040
+ Showing ${newsItems.length} article${newsItems.length !== 1 ? 's' : ''} •
1041
+ <span style="color: var(--accent-blue); font-weight: 600;">Last updated: ${new Date().toLocaleTimeString('en-US')}</span>
1042
+ </span>
1043
+ </div>
1044
  `;
1045
  } else {
1046
+ document.getElementById('news-list').innerHTML = `
1047
+ <div class="alert alert-warning" style="text-align: center; padding: 40px;">
1048
+ <div style="font-size: 48px; margin-bottom: 15px;">📰</div>
1049
+ <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">No news articles found</div>
1050
+ <div style="font-size: 14px; color: var(--text-secondary);">
1051
+ News articles will appear here once they are analyzed and stored in the database.
1052
+ </div>
1053
+ </div>
1054
+ `;
1055
  }
1056
  } catch (error) {
1057
  console.error('Error loading news:', error);
1058
+ showError('Error loading news');
1059
+ document.getElementById('news-list').innerHTML = `
1060
+ <div class="alert alert-error" style="text-align: center; padding: 40px;">
1061
+ <div style="font-size: 48px; margin-bottom: 15px;">❌</div>
1062
+ <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">Error loading news</div>
1063
+ <div style="font-size: 14px; color: var(--text-secondary);">
1064
+ ${error.message || 'Failed to fetch news articles. Please try again later.'}
1065
+ </div>
1066
+ </div>
1067
+ `;
1068
  }
1069
  }
1070
 
 
1090
  <thead>
1091
  <tr>
1092
  <th>ID</th>
1093
+ <th>Name</th>
1094
+ <th>Category</th>
1095
+ <th>Type</th>
1096
+ <th>Status</th>
1097
+ <th>Details</th>
1098
  </tr>
1099
  </thead>
1100
  <tbody>
1101
  ${providers.map(provider => {
1102
  const status = provider.status || 'unknown';
1103
  const statusConfig = {
1104
+ 'VALID': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ Valid' },
1105
+ 'validated': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ Valid' },
1106
+ 'available': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ Available' },
1107
+ 'online': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ Online' },
1108
+ 'CONDITIONALLY_AVAILABLE': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ Conditional' },
1109
+ 'INVALID': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ Invalid' },
1110
+ 'unvalidated': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ Unvalidated' },
1111
+ 'not_loaded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ Not Loaded' },
1112
+ 'offline': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ Offline' },
1113
+ 'degraded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ Degraded' }
1114
  };
1115
+ const statusInfo = statusConfig[status] || { color: 'var(--text-secondary)', bg: 'rgba(156, 163, 175, 0.2)', text: '❓ Unknown' };
1116
 
1117
  return `
1118
  <tr>
 
1137
  </table>
1138
  </div>
1139
  <div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
1140
+ <strong>Total Providers:</strong> ${providersData.total || providers.length}
1141
  </div>
1142
  `;
1143
  } else {
1144
+ providersDiv.innerHTML = '<div class="alert alert-warning">No providers found</div>';
1145
  }
1146
  }
1147
 
 
1183
 
1184
  } catch (error) {
1185
  console.error('Error loading providers:', error);
1186
+ showError('Error loading providers');
1187
  const providersDiv = document.getElementById('providers-list');
1188
  if (providersDiv) {
1189
+ providersDiv.innerHTML = '<div class="alert alert-error">Error loading providers</div>';
1190
  }
1191
  }
1192
  }
 
1195
  async function searchResources() {
1196
  const query = document.getElementById('search-resources').value;
1197
  if (!query.trim()) {
1198
+ showError('Please enter a search query');
1199
  return;
1200
  }
1201
 
1202
  const resultsDiv = document.getElementById('search-results');
1203
+ resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Searching...</div>';
1204
 
1205
  try {
1206
  const response = await fetch(`/api/resources/search?q=${encodeURIComponent(query)}`);
 
1210
  resultsDiv.innerHTML = `
1211
  <div style="margin-top: 15px;">
1212
  <div style="margin-bottom: 10px; color: var(--text-secondary);">
1213
+ ${data.count || data.resources.length} result(s) found
1214
  </div>
1215
  <div style="display: grid; gap: 10px;">
1216
  ${data.resources.map(resource => `
 
1219
  <div>
1220
  <strong style="font-size: 16px;">${resource.name || 'Unknown'}</strong>
1221
  <div style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">
1222
+ Category: ${resource.category || 'N/A'}
1223
  </div>
1224
  ${resource.base_url ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 3px; font-family: monospace;">
1225
  ${resource.base_url}
 
1227
  </div>
1228
  ${resource.free !== undefined ? `
1229
  <span style="padding: 5px 10px; border-radius: 5px; background: ${resource.free ? 'rgba(16, 185, 129, 0.2)' : 'rgba(245, 158, 11, 0.2)'}; color: ${resource.free ? 'var(--success)' : 'var(--warning)'}; font-size: 12px;">
1230
+ ${resource.free ? '🆓 Free' : '💰 Paid'}
1231
  </span>
1232
  ` : ''}
1233
  </div>
 
1237
  </div>
1238
  `;
1239
  } else {
1240
+ resultsDiv.innerHTML = '<div class="alert alert-warning" style="margin-top: 15px;">No results found</div>';
1241
  }
1242
  } catch (error) {
1243
  console.error('Search error:', error);
1244
+ resultsDiv.innerHTML = '<div class="alert alert-error" style="margin-top: 15px;">Search error</div>';
1245
+ showError('Search error');
1246
  }
1247
  }
1248
 
 
1261
 
1262
  statusDiv.innerHTML = `
1263
  <div class="alert ${healthClass}">
1264
+ <h4 style="margin-bottom: 10px;">System Status</h4>
1265
  <div style="display: grid; gap: 5px;">
1266
+ <div><strong>Overall Status:</strong> ${health}</div>
1267
+ <div><strong>Total APIs:</strong> ${statusData.total_apis || 0}</div>
1268
+ <div><strong>Online:</strong> ${statusData.online || 0}</div>
1269
+ <div><strong>Degraded:</strong> ${statusData.degraded || 0}</div>
1270
+ <div><strong>Offline:</strong> ${statusData.offline || 0}</div>
1271
+ <div><strong>Avg Response Time:</strong> ${statusData.avg_response_time_ms || 0}ms</div>
1272
+ ${statusData.last_update ? `<div><strong>Last Update:</strong> ${new Date(statusData.last_update).toLocaleString('en-US')}</div>` : ''}
1273
  </div>
1274
  </div>
1275
  `;
1276
  } catch (statusError) {
1277
+ document.getElementById('diagnostics-status').innerHTML = '<div class="alert alert-error">Error loading system status</div>';
1278
  }
1279
 
1280
  // Load error logs
 
1291
  ${errors.slice(0, 10).map(error => `
1292
  <div style="padding: 15px; background: rgba(239, 68, 68, 0.1); border-left: 4px solid var(--danger); border-radius: 5px;">
1293
  <div style="font-weight: 600; color: var(--danger); margin-bottom: 5px;">
1294
+ ${error.message || error.error_message || error.type || 'Error'}
1295
  </div>
1296
+ ${error.error_type ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">Type: ${error.error_type}</div>` : ''}
1297
  ${error.provider ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">Provider: ${error.provider}</div>` : ''}
1298
  <div style="font-size: 11px; color: var(--text-secondary); margin-top: 5px;">
1299
+ ${error.timestamp ? new Date(error.timestamp).toLocaleString('en-US') : ''}
1300
  </div>
1301
  </div>
1302
  `).join('')}
1303
  </div>
1304
  ${errors.length > 10 ? `<div style="margin-top: 10px; text-align: center; color: var(--text-secondary); font-size: 12px;">
1305
+ Showing ${Math.min(10, errors.length)} of ${errors.length} errors
1306
  </div>` : ''}
1307
  `;
1308
  } else {
1309
+ errorsDiv.innerHTML = '<div class="alert alert-success">No errors found ✅</div>';
1310
  }
1311
  } catch (errorsError) {
1312
+ document.getElementById('error-logs').innerHTML = '<div class="alert alert-warning">Error loading error logs</div>';
1313
  }
1314
 
1315
  // Load recent logs
 
1336
  ${level}
1337
  </div>
1338
  <div style="font-size: 11px; color: var(--text-secondary);">
1339
+ ${log.timestamp ? new Date(log.timestamp).toLocaleString('en-US') : ''}
1340
  </div>
1341
  </div>
1342
  <div style="font-size: 13px; color: var(--text-primary);">
 
1349
  </div>
1350
  `;
1351
  } else {
1352
+ logsDiv.innerHTML = '<div class="alert alert-warning">No logs found</div>';
1353
  }
1354
  } catch (logsError) {
1355
+ document.getElementById('recent-logs').innerHTML = '<div class="alert alert-warning">Error loading logs</div>';
1356
  }
1357
  } catch (error) {
1358
  console.error('Error loading diagnostics:', error);
1359
+ showError('Error loading diagnostics');
1360
  }
1361
  }
1362
 
 
1367
  const data = await response.json();
1368
 
1369
  if (data.success) {
1370
+ showSuccess('Diagnostics completed successfully');
1371
  setTimeout(loadDiagnostics, 1000);
1372
  } else {
1373
+ showError(data.error || 'Error running diagnostics');
1374
  }
1375
  } catch (error) {
1376
+ showError('Error running diagnostics: ' + error.message);
1377
  }
1378
  }
1379
 
 
1384
  const bodyText = document.getElementById('api-body').value;
1385
 
1386
  if (!endpoint) {
1387
+ showError('Please select an endpoint');
1388
  return;
1389
  }
1390
 
1391
  const resultDiv = document.getElementById('api-result');
1392
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Sending request...</div>';
1393
 
1394
  try {
1395
  const options = { method };
 
1401
  body = JSON.parse(bodyText);
1402
  options.headers = { 'Content-Type': 'application/json' };
1403
  } catch (e) {
1404
+ showError('Invalid JSON in body');
1405
+ resultDiv.innerHTML = '<div class="alert alert-error">JSON parsing error</div>';
1406
  return;
1407
  }
1408
  }
 
1432
  <div class="alert ${statusClass}" style="margin-bottom: 15px;">
1433
  <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
1434
  <div>
1435
+ <strong>${statusEmoji} Status:</strong> ${response.status} ${response.statusText}
1436
  </div>
1437
  <div style="font-size: 12px; color: var(--text-secondary);">
1438
+ Response Time: ${responseTime}ms
1439
  </div>
1440
  </div>
1441
  </div>
1442
  <div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px;">
1443
+ <h4 style="margin-bottom: 10px;">Response:</h4>
1444
  <pre style="background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 5px; overflow-x: auto; margin-top: 10px; font-size: 12px; max-height: 500px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
1445
  </div>
1446
  <div style="margin-top: 10px; padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 5px; font-size: 12px; color: var(--text-secondary);">
 
1451
  } catch (error) {
1452
  resultDiv.innerHTML = `
1453
  <div class="alert alert-error" style="margin-top: 20px;">
1454
+ <h4>Error:</h4>
1455
  <p>${error.message}</p>
1456
  </div>
1457
  `;
1458
+ showError('API test error: ' + error.message);
1459
  }
1460
  }
1461
 
 
1635
 
1636
  if (!data.ok) {
1637
  console.warn('API registry not available:', data.error);
1638
+ const registryContainer = document.getElementById('api-registry-section');
1639
+ if (registryContainer) {
1640
+ registryContainer.innerHTML = `
1641
+ <div class="alert alert-warning" style="padding: 30px; text-align: center;">
1642
+ <div style="font-size: 48px; margin-bottom: 15px;">📚</div>
1643
+ <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">API Registry Not Available</div>
1644
+ <div style="font-size: 14px; color: var(--text-secondary);">
1645
+ ${data.error || 'API registry file not found'}
1646
+ </div>
1647
+ </div>
1648
+ `;
1649
+ }
1650
  return;
1651
  }
1652
 
 
1654
  if (registryContainer) {
1655
  const metadata = data.metadata || {};
1656
  const categories = data.categories || [];
1657
+ const rawFiles = data.raw_files_preview || [];
1658
 
1659
  registryContainer.innerHTML = `
1660
+ <div style="background: rgba(31, 41, 55, 0.6); border-radius: 16px; padding: 24px; margin-bottom: 20px;">
1661
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;">
1662
+ <div>
1663
+ <h3 style="margin: 0 0 8px 0; color: var(--text-primary); font-size: 24px; font-weight: 700;">
1664
+ 📚 ${metadata.name || 'API Registry'}
1665
+ </h3>
1666
+ <p style="margin: 0; color: var(--text-secondary); font-size: 14px;">
1667
+ ${metadata.description || 'Comprehensive API registry for cryptocurrency data sources'}
1668
+ </p>
1669
+ </div>
1670
+ <div style="padding: 12px 20px; background: rgba(59, 130, 246, 0.15); border-radius: 10px;">
1671
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 4px;">Version</div>
1672
+ <div style="font-size: 18px; font-weight: 700; color: var(--accent-blue);">${metadata.version || 'N/A'}</div>
1673
+ </div>
1674
+ </div>
1675
+
1676
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px;">
1677
+ <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border-left: 4px solid var(--success);">
1678
+ <div style="font-size: 28px; font-weight: 800; color: var(--success); margin-bottom: 5px;">
1679
+ ${categories.length}
1680
+ </div>
1681
+ <div style="font-size: 12px; color: var(--text-secondary);">Categories</div>
1682
+ </div>
1683
+ <div style="padding: 15px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; border-left: 4px solid var(--accent-blue);">
1684
+ <div style="font-size: 28px; font-weight: 800; color: var(--accent-blue); margin-bottom: 5px;">
1685
+ ${data.total_raw_files || 0}
1686
+ </div>
1687
+ <div style="font-size: 12px; color: var(--text-secondary);">Total Files</div>
1688
+ </div>
1689
+ ${metadata.created_at ? `
1690
+ <div style="padding: 15px; background: rgba(139, 92, 246, 0.1); border-radius: 10px; border-left: 4px solid var(--accent-purple);">
1691
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Created</div>
1692
+ <div style="font-size: 14px; font-weight: 600; color: var(--accent-purple);">
1693
+ ${new Date(metadata.created_at).toLocaleDateString('en-US')}
1694
+ </div>
1695
+ </div>
1696
+ ` : ''}
1697
+ </div>
1698
+
1699
+ ${categories.length > 0 ? `
1700
+ <div style="margin-bottom: 25px;">
1701
+ <h4 style="margin: 0 0 15px 0; color: var(--text-primary); font-size: 18px; font-weight: 600;">
1702
+ 📂 Categories
1703
+ </h4>
1704
+ <div style="display: flex; flex-wrap: wrap; gap: 10px;">
1705
+ ${categories.map(cat => `
1706
+ <span style="padding: 8px 16px; background: rgba(59, 130, 246, 0.15); border-radius: 8px; font-size: 13px; font-weight: 600; color: var(--accent-blue);">
1707
+ ${cat.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
1708
+ </span>
1709
+ `).join('')}
1710
+ </div>
1711
+ </div>
1712
+ ` : ''}
1713
+
1714
+ ${rawFiles.length > 0 ? `
1715
+ <div>
1716
+ <h4 style="margin: 0 0 15px 0; color: var(--text-primary); font-size: 18px; font-weight: 600;">
1717
+ 📄 Sample Files (${rawFiles.length} of ${data.total_raw_files || 0})
1718
+ </h4>
1719
+ <div style="display: grid; gap: 10px; max-height: 400px; overflow-y: auto;">
1720
+ ${rawFiles.map(file => `
1721
+ <div style="padding: 15px; background: rgba(17, 24, 39, 0.6); border-radius: 10px; border-left: 3px solid var(--accent-blue);">
1722
+ <div style="font-weight: 600; color: var(--text-primary); margin-bottom: 5px; font-size: 14px;">
1723
+ ${file.filename || 'Unknown file'}
1724
+ </div>
1725
+ <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 8px;">
1726
+ Size: ${file.size ? (file.size / 1024).toFixed(1) + ' KB' : file.full_size ? (file.full_size / 1024).toFixed(1) + ' KB' : 'N/A'}
1727
+ </div>
1728
+ ${file.preview ? `
1729
+ <pre style="background: rgba(0, 0, 0, 0.3); padding: 10px; border-radius: 5px; font-size: 11px; color: var(--text-secondary); overflow-x: auto; margin: 0; max-height: 100px; overflow-y: auto;">${file.preview}</pre>
1730
+ ` : ''}
1731
+ </div>
1732
+ `).join('')}
1733
+ </div>
1734
+ </div>
1735
+ ` : ''}
1736
  </div>
1737
  `;
1738
  }
 
1741
  const metadataContainer = document.getElementById('api-registry-metadata');
1742
  if (metadataContainer) {
1743
  metadataContainer.innerHTML = `
1744
+ <div style="background: rgba(31, 41, 55, 0.6); border-radius: 16px; padding: 24px;">
1745
+ <h4 style="margin: 0 0 15px 0; color: var(--text-primary); font-size: 18px; font-weight: 600;">Metadata</h4>
1746
+ <pre style="background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; color: var(--text-secondary);">${JSON.stringify(metadata, null, 2)}</pre>
1747
  </div>
1748
  `;
1749
  }
1750
  } catch (error) {
1751
  console.error('Error loading API registry:', error);
1752
+ const registryContainer = document.getElementById('api-registry-section');
1753
+ if (registryContainer) {
1754
+ registryContainer.innerHTML = `
1755
+ <div class="alert alert-error" style="padding: 30px; text-align: center;">
1756
+ <div style="font-size: 48px; margin-bottom: 15px;">❌</div>
1757
+ <div style="font-size: 18px; font-weight: 600; margin-bottom: 8px;">Error Loading API Registry</div>
1758
+ <div style="font-size: 14px; color: var(--text-secondary);">
1759
+ ${error.message || 'Failed to load API registry data'}
1760
+ </div>
1761
+ </div>
1762
+ `;
1763
+ }
1764
  }
1765
  }
1766
 
templates/index.html CHANGED
@@ -1875,28 +1875,28 @@
1875
  <!-- Loading Overlay -->
1876
  <div class="loading-overlay" id="loadingOverlay">
1877
  <div class="loading-spinner-large"></div>
1878
- <div class="loading-text" id="loadingText">در حال بارگذاری...</div>
1879
  </div>
1880
 
1881
  <!-- Feedback Overlay -->
1882
  <div class="feedback-overlay" id="feedbackOverlay">
1883
  <div class="feedback-card">
1884
  <div class="feedback-icon" id="feedbackIcon">✅</div>
1885
- <div class="feedback-title" id="feedbackTitle">موفق!</div>
1886
- <div class="feedback-message" id="feedbackMessage">عملیات با موفقیت انجام شد</div>
1887
- <button class="refresh-btn ripple" onclick="hideFeedback()">بستن</button>
1888
  </div>
1889
  </div>
1890
 
1891
  <!-- Floating Action Button -->
1892
- <button class="fab ripple" onclick="scrollToTop()" title="بازگشت به بالا">
1893
 
1894
  </button>
1895
 
1896
  <!-- WebSocket Status Indicator -->
1897
  <div id="ws-connection-status" class="ws-status-indicator disconnected">
1898
  <div id="ws-status-dot" class="status-dot status-dot-offline"></div>
1899
- <span id="ws-status-text" class="ws-status-text">در حال اتصال...</span>
1900
  <div id="online-users-badge" class="badge badge-info badge-pulse" style="margin-left: 10px;">0</div>
1901
  </div>
1902
 
@@ -1981,10 +1981,10 @@
1981
  </div>
1982
  </div>
1983
  <div class="stat-value shimmer" id="active-users-count">0</div>
1984
- <div class="stat-label">کاربران آنلاین</div>
1985
  <div class="stat-change positive">
1986
  <span>📊</span>
1987
- <span>کل نشست‌ها: <span id="total-sessions-count">0</span></span>
1988
  </div>
1989
  <div class="animated-progress"></div>
1990
  </div>
@@ -2068,7 +2068,7 @@
2068
  <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-diamond"></use></svg></span>
2069
  Live Market Data
2070
  </div>
2071
- <button class="refresh-btn ripple" onclick="loadMarketData()" data-tooltip="به‌روزرسانی داده‌های بازار">
2072
  <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span>
2073
  Refresh
2074
  </button>
@@ -2077,24 +2077,24 @@
2077
  <!-- Search Bar -->
2078
  <div class="search-container">
2079
  <span class="search-icon icon"><svg><use href="#icon-search"></use></svg></span>
2080
- <input type="text" class="search-input" id="marketSearch" placeholder="جستجوی ارز دیجیتال (مثال: Bitcoin, BTC, Ethereum)..." oninput="filterMarketTable()">
2081
  </div>
2082
 
2083
  <!-- Filter Chips -->
2084
  <div class="filter-chips">
2085
- <button class="filter-chip active" onclick="filterByCategory('all')">همه</button>
2086
  <button class="filter-chip" onclick="filterByCategory('top10')">Top 10</button>
2087
  <button class="filter-chip" onclick="filterByCategory('gainers')">
2088
  <span class="icon icon-sm status-icon-success"><svg><use href="#icon-trending-up"></use></svg></span>
2089
- در حال رشد
2090
  </button>
2091
  <button class="filter-chip" onclick="filterByCategory('losers')">
2092
  <span class="icon icon-sm status-icon-error"><svg><use href="#icon-trending-down"></use></svg></span>
2093
- در حال سقوط
2094
  </button>
2095
  <button class="filter-chip" onclick="filterByCategory('volume')">
2096
  <span class="icon icon-sm"><svg><use href="#icon-volume"></use></svg></span>
2097
- حجم بالا
2098
  </button>
2099
  </div>
2100
  <div style="overflow-x: auto;">
@@ -2132,12 +2132,27 @@
2132
 
2133
  <div class="chart-container">
2134
  <div class="section-title" style="margin-bottom: 20px;">😱 Fear & Greed Index</div>
2135
- <div style="text-align: center;">
2136
  <canvas id="gaugeChart"></canvas>
2137
- <div style="font-size: 48px; font-weight: 900; margin: 20px 0 10px;" id="sentimentValue">50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2138
  </div>
2139
- <div style="color: var(--text-secondary); font-size: 16px; font-weight: 600;"
2140
- id="sentimentText">Neutral</div>
2141
  </div>
2142
  </div>
2143
  </div>
@@ -2985,7 +3000,7 @@ Crypto market is bullish today</textarea>
2985
  // Show loading state
2986
  const marketTableBody = document.getElementById('marketTableBody');
2987
  if (marketTableBody) {
2988
- 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>';
2989
  }
2990
 
2991
  showProgress(60);
@@ -3001,11 +3016,11 @@ Crypto market is bullish today</textarea>
3001
  showProgress(80);
3002
 
3003
  // Check if responses are OK
3004
- if (!marketRes.ok) throw new Error(`خطا در دریافت داده‌های بازار: ${marketRes.status}`);
3005
- if (!statsRes.ok) throw new Error(`خطا در دریافت آمار: ${statsRes.status}`);
3006
- if (!sentimentRes.ok) throw new Error(`خطا در دریافت احساسات: ${sentimentRes.status}`);
3007
- if (!trendingRes.ok) throw new Error(`خطا در دریافت ترندها: ${trendingRes.status}`);
3008
- if (!defiRes.ok) throw new Error(`خطا در دریافت DeFi: ${defiRes.status}`);
3009
 
3010
  const [market, stats, sentiment, trending, defi] = await Promise.all([
3011
  marketRes.json(),
@@ -3017,30 +3032,30 @@ Crypto market is bullish today</textarea>
3017
 
3018
  // Validate data with more detailed checks
3019
  if (!market || !Array.isArray(market.cryptocurrencies)) {
3020
- throw new Error('داده‌های بازار نامعتبر است: cryptocurrencies array not found');
3021
  }
3022
  if (!stats || typeof stats !== 'object' || stats === null) {
3023
  console.error('Invalid stats:', stats);
3024
- throw new Error('آمار نامعتبر است: stats object not found');
3025
  }
3026
  // Check if stats.market exists and is an object
3027
  if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) {
3028
  console.error('Invalid stats.market:', stats.market);
3029
  console.error('Full stats object:', JSON.stringify(stats, null, 2));
3030
- throw new Error('آمار نامعتبر است: stats.market object not found');
3031
  }
3032
  if (!sentiment || typeof sentiment !== 'object' || sentiment === null) {
3033
- throw new Error('داده‌های احساسات نامعتبر است: sentiment object not found');
3034
  }
3035
  // Note: sentiment can have different structures:
3036
  // - sentiment.fear_greed_index (from /api/sentiment)
3037
  // - sentiment.fear_greed_value (from /api/stats)
3038
  // So we don't validate the exact structure here
3039
  if (!trending || !Array.isArray(trending.trending)) {
3040
- throw new Error('داده‌های ترند نامعتبر است: trending array not found');
3041
  }
3042
  if (!defi || typeof defi !== 'object' || defi === null) {
3043
- throw new Error('داده‌های DeFi نامعتبر است: defi object not found');
3044
  }
3045
 
3046
  // Call updateStats with validated data - double check before calling
@@ -3048,7 +3063,7 @@ Crypto market is bullish today</textarea>
3048
  updateStats(stats, sentiment);
3049
  } else {
3050
  console.error('Failed final validation before updateStats:', { stats, sentiment });
3051
- throw new Error('داده‌های stats.market نامعتبر است');
3052
  }
3053
  updateMarketTable(market.cryptocurrencies);
3054
  updateTrending(trending.trending);
@@ -3060,12 +3075,12 @@ Crypto market is bullish today</textarea>
3060
  if (marketTableBody) {
3061
  marketTableBody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">
3062
  <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
3063
- <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
3064
- <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'خطای نامشخص'}</div>
3065
- <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>
3066
  </td></tr>`;
3067
  }
3068
- showToast('❌ خطا در بارگذاری داده‌های بازار: ' + (error.message || 'خطای نامشخص'), 'error');
3069
  }
3070
  }
3071
 
@@ -3162,7 +3177,7 @@ Crypto market is bullish today</textarea>
3162
  if (!cryptos || !Array.isArray(cryptos) || cryptos.length === 0) {
3163
  const tbody = document.getElementById('marketTableBody');
3164
  if (tbody) {
3165
- tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ داده‌ای یافت نشد</td></tr>';
3166
  }
3167
  return;
3168
  }
@@ -3179,7 +3194,7 @@ Crypto market is bullish today</textarea>
3179
  const marketCap = crypto.market_cap || 0;
3180
  const volume24h = crypto.volume_24h || 0;
3181
  const symbol = crypto.symbol || 'N/A';
3182
- const name = crypto.name || 'نامشخص';
3183
  const changeClass = change24h >= 0 ? 'positive' : 'negative';
3184
  const changeIcon = change24h >= 0 ? '📈' : '📉';
3185
 
@@ -3207,7 +3222,7 @@ Crypto market is bullish today</textarea>
3207
  console.error('Error updating market table:', error);
3208
  const tbody = document.getElementById('marketTableBody');
3209
  if (tbody) {
3210
- tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش داده‌ها</td></tr>';
3211
  }
3212
  }
3213
  }
@@ -3218,12 +3233,12 @@ Crypto market is bullish today</textarea>
3218
  if (!grid) return;
3219
 
3220
  if (!trending || !Array.isArray(trending) || trending.length === 0) {
3221
- grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ ترندی یافت نشد</div>';
3222
  return;
3223
  }
3224
 
3225
  grid.innerHTML = trending.map((coin, index) => {
3226
- const name = coin.name || 'نامشخص';
3227
  const symbol = coin.symbol || 'N/A';
3228
  return `
3229
  <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;">
@@ -3240,7 +3255,7 @@ Crypto market is bullish today</textarea>
3240
  console.error('Error updating trending:', error);
3241
  const grid = document.getElementById('trendingGrid');
3242
  if (grid) {
3243
- grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش ترندها</div>';
3244
  }
3245
  }
3246
  }
@@ -3260,7 +3275,7 @@ Crypto market is bullish today</textarea>
3260
  </div>
3261
  <div style="display: grid; gap: 12px;">
3262
  ${protocols.length > 0 ? protocols.map((p, i) => {
3263
- const name = p.name || 'نامشخص';
3264
  const chain = p.chain || 'N/A';
3265
  const tvl = p.tvl || 0;
3266
  const change24h = p.change_24h || 0;
@@ -3283,14 +3298,14 @@ Crypto market is bullish today</textarea>
3283
  </div>
3284
  </div>
3285
  `;
3286
- }).join('') : '<div class="empty-state"><div class="empty-state-icon">📦</div><div>هیچ پروتکلی یافت نشد</div></div>'}
3287
  </div>
3288
  `;
3289
  } catch (error) {
3290
  console.error('Error updating DeFi:', error);
3291
  const list = document.getElementById('defiList');
3292
  if (list) {
3293
- list.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش داده‌های DeFi</div>';
3294
  }
3295
  }
3296
  }
@@ -3336,29 +3351,81 @@ Crypto market is bullish today</textarea>
3336
  }
3337
 
3338
  function updateCharts(market, sentiment) {
3339
- const btcDom = market.global.btc_dominance;
3340
- const ethDom = market.global.eth_dominance;
3341
- charts.dominance.data.datasets[0].data = [btcDom, ethDom, 100 - btcDom - ethDom];
3342
  charts.dominance.update();
3343
 
3344
- const fg = sentiment.fear_greed_index.value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3345
  charts.gauge.data.datasets[0].data = [fg, 100 - fg];
3346
 
 
 
 
3347
  if (fg < 25) {
3348
  charts.gauge.data.datasets[0].backgroundColor[0] = '#ef4444';
 
 
3349
  } else if (fg < 45) {
3350
  charts.gauge.data.datasets[0].backgroundColor[0] = '#f59e0b';
 
 
3351
  } else if (fg < 55) {
3352
  charts.gauge.data.datasets[0].backgroundColor[0] = '#6b7280';
 
 
3353
  } else if (fg < 75) {
3354
  charts.gauge.data.datasets[0].backgroundColor[0] = '#3b82f6';
 
 
3355
  } else {
3356
  charts.gauge.data.datasets[0].backgroundColor[0] = '#10b981';
 
 
3357
  }
3358
 
3359
  charts.gauge.update();
3360
- document.getElementById('sentimentValue').textContent = fg;
3361
- document.getElementById('sentimentText').textContent = sentiment.fear_greed_index.classification;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3362
  }
3363
 
3364
  // استفاده از WebSocket Client جدید
 
1875
  <!-- Loading Overlay -->
1876
  <div class="loading-overlay" id="loadingOverlay">
1877
  <div class="loading-spinner-large"></div>
1878
+ <div class="loading-text" id="loadingText">Loading...</div>
1879
  </div>
1880
 
1881
  <!-- Feedback Overlay -->
1882
  <div class="feedback-overlay" id="feedbackOverlay">
1883
  <div class="feedback-card">
1884
  <div class="feedback-icon" id="feedbackIcon">✅</div>
1885
+ <div class="feedback-title" id="feedbackTitle">Success!</div>
1886
+ <div class="feedback-message" id="feedbackMessage">Operation completed successfully</div>
1887
+ <button class="refresh-btn ripple" onclick="hideFeedback()">Close</button>
1888
  </div>
1889
  </div>
1890
 
1891
  <!-- Floating Action Button -->
1892
+ <button class="fab ripple" onclick="scrollToTop()" title="Back to top">
1893
 
1894
  </button>
1895
 
1896
  <!-- WebSocket Status Indicator -->
1897
  <div id="ws-connection-status" class="ws-status-indicator disconnected">
1898
  <div id="ws-status-dot" class="status-dot status-dot-offline"></div>
1899
+ <span id="ws-status-text" class="ws-status-text">Connecting...</span>
1900
  <div id="online-users-badge" class="badge badge-info badge-pulse" style="margin-left: 10px;">0</div>
1901
  </div>
1902
 
 
1981
  </div>
1982
  </div>
1983
  <div class="stat-value shimmer" id="active-users-count">0</div>
1984
+ <div class="stat-label">Online Users</div>
1985
  <div class="stat-change positive">
1986
  <span>📊</span>
1987
+ <span>Total Sessions: <span id="total-sessions-count">0</span></span>
1988
  </div>
1989
  <div class="animated-progress"></div>
1990
  </div>
 
2068
  <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-diamond"></use></svg></span>
2069
  Live Market Data
2070
  </div>
2071
+ <button class="refresh-btn ripple" onclick="loadMarketData()" data-tooltip="Refresh market data">
2072
  <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span>
2073
  Refresh
2074
  </button>
 
2077
  <!-- Search Bar -->
2078
  <div class="search-container">
2079
  <span class="search-icon icon"><svg><use href="#icon-search"></use></svg></span>
2080
+ <input type="text" class="search-input" id="marketSearch" placeholder="Search cryptocurrency (e.g., Bitcoin, BTC, Ethereum)..." oninput="filterMarketTable()">
2081
  </div>
2082
 
2083
  <!-- Filter Chips -->
2084
  <div class="filter-chips">
2085
+ <button class="filter-chip active" onclick="filterByCategory('all')">All</button>
2086
  <button class="filter-chip" onclick="filterByCategory('top10')">Top 10</button>
2087
  <button class="filter-chip" onclick="filterByCategory('gainers')">
2088
  <span class="icon icon-sm status-icon-success"><svg><use href="#icon-trending-up"></use></svg></span>
2089
+ Gainers
2090
  </button>
2091
  <button class="filter-chip" onclick="filterByCategory('losers')">
2092
  <span class="icon icon-sm status-icon-error"><svg><use href="#icon-trending-down"></use></svg></span>
2093
+ Losers
2094
  </button>
2095
  <button class="filter-chip" onclick="filterByCategory('volume')">
2096
  <span class="icon icon-sm"><svg><use href="#icon-volume"></use></svg></span>
2097
+ High Volume
2098
  </button>
2099
  </div>
2100
  <div style="overflow-x: auto;">
 
2132
 
2133
  <div class="chart-container">
2134
  <div class="section-title" style="margin-bottom: 20px;">😱 Fear & Greed Index</div>
2135
+ <div style="text-align: center; padding: 20px;">
2136
  <canvas id="gaugeChart"></canvas>
2137
+ <div style="margin-top: 20px;">
2138
+ <div style="font-size: 64px; font-weight: 900; margin-bottom: 10px; line-height: 1;" id="sentimentValue">50</div>
2139
+ <div style="font-size: 20px; font-weight: 700; margin-bottom: 8px;" id="sentimentText">Neutral</div>
2140
+ <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 15px;" id="sentimentDescription">
2141
+ Market sentiment is balanced
2142
+ </div>
2143
+ <!-- Sentiment Meter Bar -->
2144
+ <div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden; margin-bottom: 15px;">
2145
+ <div id="sentimentBar" style="height: 100%; width: 50%; background: linear-gradient(90deg, #ef4444 0%, #f59e0b 25%, #6b7280 50%, #3b82f6 75%, #10b981 100%); border-radius: 4px; transition: width 0.5s ease;"></div>
2146
+ </div>
2147
+ <!-- Sentiment Scale Labels -->
2148
+ <div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-secondary); margin-top: 5px;">
2149
+ <span>Extreme Fear</span>
2150
+ <span>Fear</span>
2151
+ <span>Neutral</span>
2152
+ <span>Greed</span>
2153
+ <span>Extreme Greed</span>
2154
+ </div>
2155
  </div>
 
 
2156
  </div>
2157
  </div>
2158
  </div>
 
3000
  // Show loading state
3001
  const marketTableBody = document.getElementById('marketTableBody');
3002
  if (marketTableBody) {
3003
+ 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);">Loading market data...</div></td></tr>';
3004
  }
3005
 
3006
  showProgress(60);
 
3016
  showProgress(80);
3017
 
3018
  // Check if responses are OK
3019
+ if (!marketRes.ok) throw new Error(`Error fetching market data: ${marketRes.status}`);
3020
+ if (!statsRes.ok) throw new Error(`Error fetching stats: ${statsRes.status}`);
3021
+ if (!sentimentRes.ok) throw new Error(`Error fetching sentiment: ${sentimentRes.status}`);
3022
+ if (!trendingRes.ok) throw new Error(`Error fetching trending: ${trendingRes.status}`);
3023
+ if (!defiRes.ok) throw new Error(`Error fetching DeFi: ${defiRes.status}`);
3024
 
3025
  const [market, stats, sentiment, trending, defi] = await Promise.all([
3026
  marketRes.json(),
 
3032
 
3033
  // Validate data with more detailed checks
3034
  if (!market || !Array.isArray(market.cryptocurrencies)) {
3035
+ throw new Error('Invalid market data: cryptocurrencies array not found');
3036
  }
3037
  if (!stats || typeof stats !== 'object' || stats === null) {
3038
  console.error('Invalid stats:', stats);
3039
+ throw new Error('Invalid stats: stats object not found');
3040
  }
3041
  // Check if stats.market exists and is an object
3042
  if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) {
3043
  console.error('Invalid stats.market:', stats.market);
3044
  console.error('Full stats object:', JSON.stringify(stats, null, 2));
3045
+ throw new Error('Invalid stats: stats.market object not found');
3046
  }
3047
  if (!sentiment || typeof sentiment !== 'object' || sentiment === null) {
3048
+ throw new Error('Invalid sentiment data: sentiment object not found');
3049
  }
3050
  // Note: sentiment can have different structures:
3051
  // - sentiment.fear_greed_index (from /api/sentiment)
3052
  // - sentiment.fear_greed_value (from /api/stats)
3053
  // So we don't validate the exact structure here
3054
  if (!trending || !Array.isArray(trending.trending)) {
3055
+ throw new Error('Invalid trending data: trending array not found');
3056
  }
3057
  if (!defi || typeof defi !== 'object' || defi === null) {
3058
+ throw new Error('Invalid DeFi data: defi object not found');
3059
  }
3060
 
3061
  // Call updateStats with validated data - double check before calling
 
3063
  updateStats(stats, sentiment);
3064
  } else {
3065
  console.error('Failed final validation before updateStats:', { stats, sentiment });
3066
+ throw new Error('Invalid stats.market data');
3067
  }
3068
  updateMarketTable(market.cryptocurrencies);
3069
  updateTrending(trending.trending);
 
3075
  if (marketTableBody) {
3076
  marketTableBody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">
3077
  <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
3078
+ <div style="font-weight: 600; margin-bottom: 5px;">Error loading data</div>
3079
+ <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'Unknown error'}</div>
3080
+ <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;">Try Again</button>
3081
  </td></tr>`;
3082
  }
3083
+ showToast('❌ Error loading market data: ' + (error.message || 'Unknown error'), 'error');
3084
  }
3085
  }
3086
 
 
3177
  if (!cryptos || !Array.isArray(cryptos) || cryptos.length === 0) {
3178
  const tbody = document.getElementById('marketTableBody');
3179
  if (tbody) {
3180
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);">No data found</td></tr>';
3181
  }
3182
  return;
3183
  }
 
3194
  const marketCap = crypto.market_cap || 0;
3195
  const volume24h = crypto.volume_24h || 0;
3196
  const symbol = crypto.symbol || 'N/A';
3197
+ const name = crypto.name || 'Unknown';
3198
  const changeClass = change24h >= 0 ? 'positive' : 'negative';
3199
  const changeIcon = change24h >= 0 ? '📈' : '📉';
3200
 
 
3222
  console.error('Error updating market table:', error);
3223
  const tbody = document.getElementById('marketTableBody');
3224
  if (tbody) {
3225
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">Error displaying data</td></tr>';
3226
  }
3227
  }
3228
  }
 
3233
  if (!grid) return;
3234
 
3235
  if (!trending || !Array.isArray(trending) || trending.length === 0) {
3236
+ grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No trending coins found</div>';
3237
  return;
3238
  }
3239
 
3240
  grid.innerHTML = trending.map((coin, index) => {
3241
+ const name = coin.name || 'Unknown';
3242
  const symbol = coin.symbol || 'N/A';
3243
  return `
3244
  <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;">
 
3255
  console.error('Error updating trending:', error);
3256
  const grid = document.getElementById('trendingGrid');
3257
  if (grid) {
3258
+ grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">Error displaying trending</div>';
3259
  }
3260
  }
3261
  }
 
3275
  </div>
3276
  <div style="display: grid; gap: 12px;">
3277
  ${protocols.length > 0 ? protocols.map((p, i) => {
3278
+ const name = p.name || 'Unknown';
3279
  const chain = p.chain || 'N/A';
3280
  const tvl = p.tvl || 0;
3281
  const change24h = p.change_24h || 0;
 
3298
  </div>
3299
  </div>
3300
  `;
3301
+ }).join('') : '<div class="empty-state"><div class="empty-state-icon">📦</div><div>No protocols found</div></div>'}
3302
  </div>
3303
  `;
3304
  } catch (error) {
3305
  console.error('Error updating DeFi:', error);
3306
  const list = document.getElementById('defiList');
3307
  if (list) {
3308
+ list.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">Error displaying DeFi data</div>';
3309
  }
3310
  }
3311
  }
 
3351
  }
3352
 
3353
  function updateCharts(market, sentiment) {
3354
+ const btcDom = market.global?.btc_dominance || market.btc_dominance || 0;
3355
+ const ethDom = market.global?.eth_dominance || 0;
3356
+ charts.dominance.data.datasets[0].data = [btcDom, ethDom, Math.max(0, 100 - btcDom - ethDom)];
3357
  charts.dominance.update();
3358
 
3359
+ // Handle different sentiment data structures
3360
+ let fg = 50;
3361
+ let classification = 'Neutral';
3362
+
3363
+ if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') {
3364
+ fg = sentiment.fear_greed_index.value || sentiment.fear_greed_index || 50;
3365
+ classification = sentiment.fear_greed_index.value_classification || sentiment.fear_greed_index.classification || 'Neutral';
3366
+ } else if (typeof sentiment.fear_greed_index === 'number') {
3367
+ fg = sentiment.fear_greed_index;
3368
+ classification = sentiment.fear_greed_label || 'Neutral';
3369
+ } else if (typeof sentiment.fear_greed_value !== 'undefined') {
3370
+ fg = sentiment.fear_greed_value;
3371
+ classification = sentiment.classification || 'Neutral';
3372
+ } else if (typeof sentiment.value !== 'undefined') {
3373
+ fg = sentiment.value;
3374
+ classification = sentiment.classification || 'Neutral';
3375
+ }
3376
+
3377
+ // Ensure fg is between 0-100
3378
+ fg = Math.max(0, Math.min(100, fg));
3379
+
3380
  charts.gauge.data.datasets[0].data = [fg, 100 - fg];
3381
 
3382
+ let barColor = '#6b7280';
3383
+ let description = 'Market sentiment is balanced';
3384
+
3385
  if (fg < 25) {
3386
  charts.gauge.data.datasets[0].backgroundColor[0] = '#ef4444';
3387
+ barColor = '#ef4444';
3388
+ description = 'Extreme fear - investors are very worried, potential buying opportunity';
3389
  } else if (fg < 45) {
3390
  charts.gauge.data.datasets[0].backgroundColor[0] = '#f59e0b';
3391
+ barColor = '#f59e0b';
3392
+ description = 'Fear - market shows concern, cautious sentiment';
3393
  } else if (fg < 55) {
3394
  charts.gauge.data.datasets[0].backgroundColor[0] = '#6b7280';
3395
+ barColor = '#6b7280';
3396
+ description = 'Neutral - balanced market sentiment';
3397
  } else if (fg < 75) {
3398
  charts.gauge.data.datasets[0].backgroundColor[0] = '#3b82f6';
3399
+ barColor = '#3b82f6';
3400
+ description = 'Greed - optimistic market sentiment';
3401
  } else {
3402
  charts.gauge.data.datasets[0].backgroundColor[0] = '#10b981';
3403
+ barColor = '#10b981';
3404
+ description = 'Extreme greed - very optimistic, potential correction risk';
3405
  }
3406
 
3407
  charts.gauge.update();
3408
+
3409
+ const sentimentValueEl = document.getElementById('sentimentValue');
3410
+ const sentimentTextEl = document.getElementById('sentimentText');
3411
+ const sentimentDescEl = document.getElementById('sentimentDescription');
3412
+ const sentimentBarEl = document.getElementById('sentimentBar');
3413
+
3414
+ if (sentimentValueEl) {
3415
+ sentimentValueEl.textContent = fg;
3416
+ sentimentValueEl.style.color = barColor;
3417
+ }
3418
+ if (sentimentTextEl) {
3419
+ sentimentTextEl.textContent = classification;
3420
+ sentimentTextEl.style.color = barColor;
3421
+ }
3422
+ if (sentimentDescEl) {
3423
+ sentimentDescEl.textContent = description;
3424
+ }
3425
+ if (sentimentBarEl) {
3426
+ sentimentBarEl.style.width = fg + '%';
3427
+ sentimentBarEl.style.background = `linear-gradient(90deg, #ef4444 0%, #f59e0b 25%, #6b7280 50%, #3b82f6 75%, #10b981 100%)`;
3428
+ }
3429
  }
3430
 
3431
  // استفاده از WebSocket Client جدید