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