Really-amin commited on
Commit
b5dc8a3
·
verified ·
1 Parent(s): 89dbf23

Update INDEX.html

Browse files
Files changed (1) hide show
  1. INDEX.html +469 -681
INDEX.html CHANGED
@@ -1,9 +1,11 @@
1
  <!DOCTYPE html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Enhanced Crypto Data Tracker</title>
 
 
7
  <style>
8
  * {
9
  margin: 0;
@@ -11,866 +13,652 @@
11
  box-sizing: border-box;
12
  }
13
 
 
 
 
 
 
 
 
 
 
 
14
  body {
15
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
16
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
17
  min-height: 100vh;
18
  padding: 20px;
19
  }
20
 
21
  .container {
22
- max-width: 1600px;
23
  margin: 0 auto;
24
  }
25
 
26
- header {
 
27
  background: rgba(255, 255, 255, 0.1);
28
- backdrop-filter: blur(10px);
29
- border-radius: 15px;
30
- padding: 20px 30px;
31
- margin-bottom: 20px;
32
- display: flex;
33
- justify-content: space-between;
34
- align-items: center;
35
  }
36
 
37
- h1 {
38
- color: white;
39
- font-size: 28px;
 
40
  display: flex;
41
  align-items: center;
42
- gap: 10px;
43
  }
44
 
45
- .connection-status {
46
- display: flex;
47
  align-items: center;
48
- gap: 10px;
49
- color: white;
 
 
50
  font-size: 14px;
51
  }
52
 
53
- .status-indicator {
54
  width: 10px;
55
  height: 10px;
 
56
  border-radius: 50%;
57
- background: #10b981;
58
  animation: pulse 2s infinite;
59
  }
60
 
61
- .status-indicator.disconnected {
62
- background: #ef4444;
63
- animation: none;
64
- }
65
-
66
  @keyframes pulse {
67
  0%, 100% { opacity: 1; }
68
  50% { opacity: 0.5; }
69
  }
70
 
71
- .controls {
72
- background: rgba(255, 255, 255, 0.1);
73
- backdrop-filter: blur(10px);
74
- border-radius: 15px;
75
- padding: 20px;
76
- margin-bottom: 20px;
77
  }
78
 
79
- .controls-row {
80
- display: flex;
81
- gap: 15px;
82
- flex-wrap: wrap;
83
- align-items: center;
 
84
  }
85
 
86
- .btn {
87
- padding: 10px 20px;
88
- border: none;
89
- border-radius: 8px;
90
- cursor: pointer;
91
  font-size: 14px;
92
- font-weight: 600;
93
- transition: all 0.3s ease;
94
- display: flex;
95
- align-items: center;
96
- gap: 8px;
97
  }
98
 
99
- .btn-primary {
100
- background: white;
101
- color: #667eea;
 
102
  }
103
 
104
- .btn-primary:hover {
105
- transform: translateY(-2px);
106
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
107
  }
108
 
109
- .btn-success {
110
- background: #10b981;
111
- color: white;
112
- }
113
 
114
- .btn-danger {
115
- background: #ef4444;
116
- color: white;
 
 
 
117
  }
118
 
119
- .btn-info {
120
- background: #3b82f6;
 
 
 
121
  color: white;
 
 
 
 
122
  }
123
 
124
- .btn:disabled {
125
- opacity: 0.5;
126
- cursor: not-allowed;
127
  }
128
 
129
- .grid {
130
- display: grid;
131
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
132
- gap: 20px;
133
- margin-bottom: 20px;
134
  }
135
 
136
- .card {
137
- background: rgba(255, 255, 255, 0.1);
138
- backdrop-filter: blur(10px);
139
- border-radius: 15px;
140
- padding: 20px;
141
- color: white;
142
  }
143
 
144
- .card h2 {
145
- font-size: 18px;
146
- margin-bottom: 15px;
147
- display: flex;
148
- align-items: center;
149
- gap: 10px;
150
  }
151
 
152
- .stat-grid {
 
153
  display: grid;
154
- grid-template-columns: repeat(2, 1fr);
155
- gap: 15px;
156
  }
157
 
158
- .stat-item {
159
  background: rgba(255, 255, 255, 0.1);
160
- padding: 15px;
161
- border-radius: 10px;
162
- }
163
-
164
- .stat-label {
165
- font-size: 12px;
166
- opacity: 0.8;
167
- margin-bottom: 5px;
168
- }
169
-
170
- .stat-value {
171
- font-size: 24px;
172
- font-weight: 700;
173
  }
174
 
175
- .api-list {
176
- max-height: 400px;
177
- overflow-y: auto;
178
  }
179
 
180
- .api-item {
181
- background: rgba(255, 255, 255, 0.05);
182
- padding: 15px;
183
- border-radius: 10px;
184
- margin-bottom: 10px;
185
  display: flex;
186
  justify-content: space-between;
187
  align-items: center;
 
188
  }
189
 
190
- .api-info {
191
- flex: 1;
192
- }
193
-
194
- .api-name {
195
- font-weight: 600;
196
- margin-bottom: 5px;
197
- }
198
-
199
- .api-meta {
200
- font-size: 12px;
201
- opacity: 0.7;
202
- display: flex;
203
- gap: 15px;
204
- }
205
-
206
- .api-controls {
207
- display: flex;
208
- gap: 10px;
209
  }
210
 
211
- .small-btn {
212
- padding: 5px 12px;
213
- font-size: 12px;
214
- border-radius: 5px;
215
- border: none;
216
- cursor: pointer;
217
  background: rgba(255, 255, 255, 0.2);
218
- color: white;
219
- transition: all 0.2s;
220
- }
221
-
222
- .small-btn:hover {
223
- background: rgba(255, 255, 255, 0.3);
224
- }
225
-
226
- .status-badge {
227
- display: inline-block;
228
- padding: 4px 10px;
229
- border-radius: 12px;
230
- font-size: 11px;
231
- font-weight: 600;
232
- }
233
-
234
- .status-success {
235
- background: #10b981;
236
- color: white;
237
- }
238
-
239
- .status-pending {
240
- background: #f59e0b;
241
- color: white;
242
- }
243
-
244
- .status-failed {
245
- background: #ef4444;
246
- color: white;
247
- }
248
-
249
- .log-container {
250
- background: rgba(0, 0, 0, 0.3);
251
  border-radius: 10px;
252
- padding: 15px;
253
- max-height: 300px;
254
- overflow-y: auto;
255
- font-family: 'Courier New', monospace;
256
  font-size: 12px;
257
  }
258
 
259
- .log-entry {
260
- margin-bottom: 8px;
261
- padding: 5px;
262
- border-left: 3px solid #667eea;
263
- padding-left: 10px;
264
  }
265
 
266
- .log-time {
267
- opacity: 0.6;
268
- margin-right: 10px;
269
  }
270
 
271
- .modal {
272
- display: none;
273
- position: fixed;
274
- top: 0;
275
- left: 0;
276
- width: 100%;
277
- height: 100%;
278
- background: rgba(0, 0, 0, 0.7);
279
- z-index: 1000;
280
- justify-content: center;
281
- align-items: center;
282
  }
283
 
284
- .modal.active {
285
- display: flex;
286
  }
287
 
288
- .modal-content {
289
- background: white;
290
- border-radius: 15px;
291
- padding: 30px;
292
- max-width: 500px;
293
- width: 90%;
294
- color: #333;
295
  }
296
 
297
- .modal-header {
298
  display: flex;
299
  justify-content: space-between;
300
- align-items: center;
301
- margin-bottom: 20px;
302
- }
303
-
304
- .modal-close {
305
- background: none;
306
- border: none;
307
- font-size: 24px;
308
- cursor: pointer;
309
- color: #666;
310
  }
311
 
312
- .form-group {
313
- margin-bottom: 15px;
 
 
 
 
 
314
  }
315
 
316
- .form-label {
317
- display: block;
318
- margin-bottom: 5px;
319
- font-weight: 600;
320
- color: #333;
321
  }
322
 
323
- .form-input {
324
- width: 100%;
325
- padding: 10px;
326
- border: 1px solid #ddd;
327
- border-radius: 8px;
328
- font-size: 14px;
329
  }
330
 
331
- .form-select {
332
  width: 100%;
333
- padding: 10px;
334
- border: 1px solid #ddd;
335
- border-radius: 8px;
336
- font-size: 14px;
 
337
  }
338
 
339
- ::-webkit-scrollbar {
340
- width: 8px;
 
 
 
 
 
 
 
341
  }
342
 
343
- ::-webkit-scrollbar-track {
 
344
  background: rgba(255, 255, 255, 0.1);
345
- border-radius: 4px;
346
- }
347
-
348
- ::-webkit-scrollbar-thumb {
349
- background: rgba(255, 255, 255, 0.3);
350
- border-radius: 4px;
351
  }
352
 
353
- ::-webkit-scrollbar-thumb:hover {
354
- background: rgba(255, 255, 255, 0.5);
 
 
355
  }
356
 
357
- .toast {
358
- position: fixed;
359
- bottom: 20px;
360
- right: 20px;
361
- background: white;
362
- color: #333;
363
- padding: 15px 20px;
364
- border-radius: 10px;
365
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
366
- opacity: 0;
367
- transform: translateY(20px);
368
- transition: all 0.3s ease;
369
- z-index: 2000;
370
  }
371
 
372
- .toast.show {
373
- opacity: 1;
374
- transform: translateY(0);
 
 
 
 
 
 
375
  }
376
  </style>
377
  </head>
378
  <body>
379
  <div class="container">
380
- <header>
381
- <h1>
 
382
  <span>🚀</span>
383
- Enhanced Crypto Data Tracker
384
- </h1>
385
- <div class="connection-status">
386
- <div class="status-indicator" id="wsStatus"></div>
387
- <span id="wsStatusText">Connecting...</span>
388
- </div>
389
- </header>
390
-
391
- <div class="controls">
392
- <div class="controls-row">
393
- <button class="btn btn-primary" onclick="exportJSON()">
394
- 💾 Export JSON
395
- </button>
396
- <button class="btn btn-primary" onclick="exportCSV()">
397
- 📊 Export CSV
398
- </button>
399
- <button class="btn btn-success" onclick="createBackup()">
400
- 🔄 Create Backup
401
- </button>
402
- <button class="btn btn-info" onclick="showScheduleModal()">
403
- ⏰ Configure Schedule
404
- </button>
405
- <button class="btn btn-info" onclick="forceUpdateAll()">
406
- 🔃 Force Update All
407
- </button>
408
- <button class="btn btn-danger" onclick="clearCache()">
409
- 🗑️ Clear Cache
410
- </button>
411
  </div>
412
  </div>
413
 
414
- <div class="grid">
415
- <div class="card">
416
- <h2>📊 System Statistics</h2>
417
- <div class="stat-grid">
418
- <div class="stat-item">
419
- <div class="stat-label">Total APIs</div>
420
- <div class="stat-value" id="totalApis">0</div>
421
- </div>
422
- <div class="stat-item">
423
- <div class="stat-label">Active Tasks</div>
424
- <div class="stat-value" id="activeTasks">0</div>
425
- </div>
426
- <div class="stat-item">
427
- <div class="stat-label">Cached Data</div>
428
- <div class="stat-value" id="cachedData">0</div>
429
- </div>
430
- <div class="stat-item">
431
- <div class="stat-label">WS Connections</div>
432
- <div class="stat-value" id="wsConnections">0</div>
433
- </div>
434
- </div>
435
  </div>
 
 
 
 
 
436
 
437
- <div class="card">
438
- <h2>📈 Recent Activity</h2>
439
- <div class="log-container" id="activityLog">
440
- <div class="log-entry">
441
- <span class="log-time">--:--:--</span>
442
- Waiting for updates...
443
- </div>
444
- </div>
 
 
 
 
 
 
445
  </div>
446
  </div>
447
 
448
- <div class="card">
449
- <h2>🔌 API Sources</h2>
450
- <div class="api-list" id="apiList">
451
- Loading...
 
452
  </div>
453
  </div>
454
- </div>
455
 
456
- <!-- Schedule Modal -->
457
- <div class="modal" id="scheduleModal">
458
- <div class="modal-content">
459
- <div class="modal-header">
460
- <h2>⏰ Configure Schedule</h2>
461
- <button class="modal-close" onclick="closeScheduleModal()">×</button>
462
  </div>
463
- <div class="form-group">
464
- <label class="form-label">API Source</label>
465
- <select class="form-select" id="scheduleApiSelect"></select>
 
 
 
 
 
 
 
 
 
466
  </div>
467
- <div class="form-group">
468
- <label class="form-label">Interval (seconds)</label>
469
- <input type="number" class="form-input" id="scheduleInterval" value="60" min="10">
470
  </div>
471
- <div class="form-group">
472
- <label class="form-label">Enabled</label>
473
- <input type="checkbox" id="scheduleEnabled" checked>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  </div>
475
- <button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button>
476
  </div>
477
  </div>
478
 
479
- <!-- Toast notification -->
480
- <div class="toast" id="toast"></div>
481
-
482
  <script>
483
- let ws = null;
484
- let reconnectAttempts = 0;
485
- const maxReconnectAttempts = 5;
486
- const reconnectDelay = 3000;
487
-
488
- // Initialize WebSocket connection
489
- function initWebSocket() {
490
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
491
- const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`;
492
-
493
- ws = new WebSocket(wsUrl);
494
-
495
- ws.onopen = () => {
496
- console.log('WebSocket connected');
497
- updateWSStatus(true);
498
- reconnectAttempts = 0;
499
-
500
- // Subscribe to all updates
501
- ws.send(JSON.stringify({ type: 'subscribe_all' }));
502
-
503
- // Start heartbeat
504
- startHeartbeat();
505
- };
506
-
507
- ws.onmessage = (event) => {
508
- const message = JSON.parse(event.data);
509
- handleWSMessage(message);
510
- };
511
-
512
- ws.onerror = (error) => {
513
- console.error('WebSocket error:', error);
514
- };
515
-
516
- ws.onclose = () => {
517
- console.log('WebSocket disconnected');
518
- updateWSStatus(false);
519
- attemptReconnect();
520
- };
521
- }
522
-
523
- function attemptReconnect() {
524
- if (reconnectAttempts < maxReconnectAttempts) {
525
- reconnectAttempts++;
526
- console.log(`Reconnecting... Attempt ${reconnectAttempts}`);
527
- setTimeout(initWebSocket, reconnectDelay);
528
- }
529
- }
530
-
531
- let heartbeatInterval;
532
- function startHeartbeat() {
533
- clearInterval(heartbeatInterval);
534
- heartbeatInterval = setInterval(() => {
535
- if (ws && ws.readyState === WebSocket.OPEN) {
536
- ws.send(JSON.stringify({ type: 'ping' }));
537
- }
538
- }, 30000);
539
- }
540
-
541
- function updateWSStatus(connected) {
542
- const indicator = document.getElementById('wsStatus');
543
- const text = document.getElementById('wsStatusText');
544
-
545
- if (connected) {
546
- indicator.classList.remove('disconnected');
547
- text.textContent = 'Connected';
548
- } else {
549
- indicator.classList.add('disconnected');
550
- text.textContent = 'Disconnected';
551
- }
552
- }
553
-
554
- function handleWSMessage(message) {
555
- console.log('Received:', message);
556
-
557
- switch (message.type) {
558
- case 'api_update':
559
- handleApiUpdate(message);
560
  break;
561
- case 'status_update':
562
- handleStatusUpdate(message);
563
  break;
564
- case 'schedule_update':
565
- handleScheduleUpdate(message);
566
  break;
567
- case 'subscribed':
568
- addLog(`Subscribed to ${message.api_id || 'all updates'}`);
 
 
 
569
  break;
570
  }
571
  }
572
 
573
- function handleApiUpdate(message) {
574
- addLog(`Updated: ${message.api_id}`, 'success');
575
- loadSystemStatus();
576
- }
577
-
578
- function handleStatusUpdate(message) {
579
- addLog('System status updated');
580
- loadSystemStatus();
581
- }
582
-
583
- function handleScheduleUpdate(message) {
584
- addLog(`Schedule updated for ${message.schedule.api_id}`);
585
- loadAPIs();
586
- }
587
-
588
- function addLog(text, type = 'info') {
589
- const logContainer = document.getElementById('activityLog');
590
- const time = new Date().toLocaleTimeString();
591
-
592
- const entry = document.createElement('div');
593
- entry.className = 'log-entry';
594
- entry.innerHTML = `<span class="log-time">${time}</span>${text}`;
595
-
596
- logContainer.insertBefore(entry, logContainer.firstChild);
597
-
598
- // Keep only last 50 entries
599
- while (logContainer.children.length > 50) {
600
- logContainer.removeChild(logContainer.lastChild);
601
- }
602
- }
603
-
604
- function showToast(message, duration = 3000) {
605
- const toast = document.getElementById('toast');
606
- toast.textContent = message;
607
- toast.classList.add('show');
608
-
609
- setTimeout(() => {
610
- toast.classList.remove('show');
611
- }, duration);
612
- }
613
-
614
- // Load system status
615
- async function loadSystemStatus() {
616
- try {
617
- const response = await fetch('/api/v2/status');
618
- const data = await response.json();
619
-
620
- document.getElementById('totalApis').textContent =
621
- data.services.config_loader.apis_loaded;
622
- document.getElementById('activeTasks').textContent =
623
- data.services.scheduler.total_tasks;
624
- document.getElementById('cachedData').textContent =
625
- data.services.persistence.cached_apis;
626
- document.getElementById('wsConnections').textContent =
627
- data.services.websocket.total_connections;
628
-
629
- } catch (error) {
630
- console.error('Error loading status:', error);
631
- }
632
- }
633
-
634
- // Load APIs
635
- async function loadAPIs() {
636
- try {
637
- const response = await fetch('/api/v2/config/apis');
638
- const data = await response.json();
639
-
640
- const scheduleResponse = await fetch('/api/v2/schedule/tasks');
641
- const schedules = await scheduleResponse.json();
642
-
643
- displayAPIs(data.apis, schedules);
644
-
645
- } catch (error) {
646
- console.error('Error loading APIs:', error);
647
- }
648
- }
649
-
650
- function displayAPIs(apis, schedules) {
651
- const listElement = document.getElementById('apiList');
652
- listElement.innerHTML = '';
653
-
654
- for (const [apiId, api] of Object.entries(apis)) {
655
- const schedule = schedules[apiId] || {};
656
-
657
- const item = document.createElement('div');
658
- item.className = 'api-item';
659
- item.innerHTML = `
660
- <div class="api-info">
661
- <div class="api-name">${api.name}</div>
662
- <div class="api-meta">
663
- <span>📂 ${api.category}</span>
664
- <span>⏱️ ${schedule.interval || 300}s</span>
665
- <span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}">
666
- ${schedule.last_status || 'pending'}
667
- </span>
668
- </div>
669
- </div>
670
- <div class="api-controls">
671
- <button class="small-btn" onclick="forceUpdate('${apiId}')">🔄 Update</button>
672
- <button class="small-btn" onclick="showScheduleModalFor('${apiId}')">⚙️ Schedule</button>
673
- </div>
674
- `;
675
-
676
- listElement.appendChild(item);
677
- }
678
- }
679
-
680
- // Export functions
681
- async function exportJSON() {
682
  try {
683
- const response = await fetch('/api/v2/export/json', {
684
- method: 'POST',
685
- headers: { 'Content-Type': 'application/json' },
686
- body: JSON.stringify({ include_history: true })
687
- });
688
-
689
  const data = await response.json();
690
- showToast('✅ JSON export created!');
691
- addLog(`Exported to JSON: ${data.filepath}`);
692
-
693
- // Trigger download
694
- window.open(data.download_url, '_blank');
695
-
 
 
 
 
696
  } catch (error) {
697
- showToast(' Export failed');
698
- console.error(error);
699
  }
700
  }
701
 
702
- async function exportCSV() {
 
703
  try {
704
- const response = await fetch('/api/v2/export/csv', {
705
- method: 'POST',
706
- headers: { 'Content-Type': 'application/json' },
707
- body: JSON.stringify({ flatten: true })
708
- });
709
-
710
- const data = await response.json();
711
- showToast('✅ CSV export created!');
712
- addLog(`Exported to CSV: ${data.filepath}`);
713
-
714
- // Trigger download
715
- window.open(data.download_url, '_blank');
716
-
717
  } catch (error) {
718
- showToast(' Export failed');
719
- console.error(error);
720
  }
721
  }
722
 
723
- async function createBackup() {
 
724
  try {
725
- const response = await fetch('/api/v2/backup', {
726
- method: 'POST'
727
- });
728
-
729
- const data = await response.json();
730
- showToast('✅ Backup created!');
731
- addLog(`Backup created: ${data.backup_file}`);
732
-
733
  } catch (error) {
734
- showToast(' Backup failed');
735
- console.error(error);
736
  }
737
  }
738
 
739
- async function forceUpdate(apiId) {
 
740
  try {
741
- const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, {
742
- method: 'POST'
743
- });
744
-
745
- const data = await response.json();
746
- showToast(`✅ ${apiId} updated!`);
747
- addLog(`Forced update: ${apiId}`);
748
- loadAPIs();
749
-
 
 
 
 
750
  } catch (error) {
751
- showToast(' Update failed');
752
- console.error(error);
753
  }
754
  }
755
 
756
- async function forceUpdateAll() {
757
- showToast('🔄 Updating all APIs...');
758
- addLog('Forcing update for all APIs');
759
-
760
  try {
761
- const response = await fetch('/api/v2/config/apis');
762
- const data = await response.json();
763
-
764
- for (const apiId of Object.keys(data.apis)) {
765
- await forceUpdate(apiId);
766
- await new Promise(resolve => setTimeout(resolve, 100)); // Small delay
 
 
 
 
 
 
 
 
 
 
767
  }
768
-
769
- showToast('✅ All APIs updated!');
770
  } catch (error) {
771
- showToast(' Update failed');
772
- console.error(error);
773
  }
774
  }
775
 
776
- async function clearCache() {
777
- if (!confirm('Clear all cached data?')) return;
778
-
779
  try {
780
- const response = await fetch('/api/v2/cleanup/cache', {
781
- method: 'POST'
782
- });
783
-
784
- showToast('✅ Cache cleared!');
785
- addLog('Cache cleared');
786
- loadSystemStatus();
787
-
 
788
  } catch (error) {
789
- showToast(' Failed to clear cache');
790
- console.error(error);
791
  }
792
  }
793
 
794
- // Schedule modal functions
795
- function showScheduleModal() {
796
- loadAPISelectOptions();
797
- document.getElementById('scheduleModal').classList.add('active');
798
- }
799
-
800
- function closeScheduleModal() {
801
- document.getElementById('scheduleModal').classList.remove('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  }
803
 
804
- async function showScheduleModalFor(apiId) {
805
- await loadAPISelectOptions();
806
- document.getElementById('scheduleApiSelect').value = apiId;
807
-
808
- // Load current schedule
809
- try {
810
- const response = await fetch(`/api/v2/schedule/tasks/${apiId}`);
811
- const schedule = await response.json();
812
-
813
- document.getElementById('scheduleInterval').value = schedule.interval;
814
- document.getElementById('scheduleEnabled').checked = schedule.enabled;
815
-
816
- } catch (error) {
817
- console.error(error);
818
- }
819
-
820
- showScheduleModal();
821
  }
822
 
823
- async function loadAPISelectOptions() {
824
- try {
825
- const response = await fetch('/api/v2/config/apis');
826
- const data = await response.json();
827
-
828
- const select = document.getElementById('scheduleApiSelect');
829
- select.innerHTML = '';
830
-
831
- for (const [apiId, api] of Object.entries(data.apis)) {
832
- const option = document.createElement('option');
833
- option.value = apiId;
834
- option.textContent = api.name;
835
- select.appendChild(option);
836
- }
837
-
838
- } catch (error) {
839
- console.error(error);
840
- }
841
  }
842
 
843
- async function updateSchedule() {
844
- const apiId = document.getElementById('scheduleApiSelect').value;
845
- const interval = parseInt(document.getElementById('scheduleInterval').value);
846
- const enabled = document.getElementById('scheduleEnabled').checked;
847
-
848
- try {
849
- const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, {
850
- method: 'PUT'
851
- });
852
-
853
- const data = await response.json();
854
- showToast('✅ Schedule updated!');
855
- addLog(`Updated schedule for ${apiId}`);
856
- closeScheduleModal();
857
- loadAPIs();
858
-
859
- } catch (error) {
860
- showToast('❌ Schedule update failed');
861
- console.error(error);
862
- }
863
  }
864
 
865
- // Initialize on load
866
- window.addEventListener('load', () => {
867
- initWebSocket();
868
- loadSystemStatus();
869
- loadAPIs();
870
-
871
- // Refresh status every 30 seconds
872
- setInterval(loadSystemStatus, 30000);
 
 
873
  });
874
  </script>
875
  </body>
876
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="fa" dir="rtl">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>🚀 داشبورد هوشمند کریپتو</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
  <style>
10
  * {
11
  margin: 0;
 
13
  box-sizing: border-box;
14
  }
15
 
16
+ :root {
17
+ --primary: #667eea;
18
+ --secondary: #764ba2;
19
+ --success: #10b981;
20
+ --danger: #ef4444;
21
+ --warning: #f59e0b;
22
+ --dark: #1e293b;
23
+ --light: #f8fafc;
24
+ }
25
+
26
  body {
27
+ font-family: 'Vazirmatn', sans-serif;
28
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
29
+ color: white;
30
  min-height: 100vh;
31
  padding: 20px;
32
  }
33
 
34
  .container {
35
+ max-width: 1400px;
36
  margin: 0 auto;
37
  }
38
 
39
+ /* Header */
40
+ .header {
41
  background: rgba(255, 255, 255, 0.1);
42
+ backdrop-filter: blur(20px);
43
+ border-radius: 20px;
44
+ padding: 30px;
45
+ margin-bottom: 30px;
46
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
 
 
47
  }
48
 
49
+ .header-title {
50
+ font-size: 32px;
51
+ font-weight: 800;
52
+ margin-bottom: 20px;
53
  display: flex;
54
  align-items: center;
55
+ gap: 15px;
56
  }
57
 
58
+ .status-badge {
59
+ display: inline-flex;
60
  align-items: center;
61
+ gap: 8px;
62
+ padding: 8px 16px;
63
+ background: rgba(16, 185, 129, 0.2);
64
+ border-radius: 20px;
65
  font-size: 14px;
66
  }
67
 
68
+ .status-dot {
69
  width: 10px;
70
  height: 10px;
71
+ background: var(--success);
72
  border-radius: 50%;
 
73
  animation: pulse 2s infinite;
74
  }
75
 
 
 
 
 
 
76
  @keyframes pulse {
77
  0%, 100% { opacity: 1; }
78
  50% { opacity: 0.5; }
79
  }
80
 
81
+ /* Stats Grid */
82
+ .stats-grid {
83
+ display: grid;
84
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
85
+ gap: 20px;
86
+ margin-bottom: 30px;
87
  }
88
 
89
+ .stat-card {
90
+ background: rgba(255, 255, 255, 0.1);
91
+ backdrop-filter: blur(20px);
92
+ border-radius: 15px;
93
+ padding: 25px;
94
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
95
  }
96
 
97
+ .stat-label {
 
 
 
 
98
  font-size: 14px;
99
+ opacity: 0.8;
100
+ margin-bottom: 10px;
 
 
 
101
  }
102
 
103
+ .stat-value {
104
+ font-size: 32px;
105
+ font-weight: 800;
106
+ margin-bottom: 10px;
107
  }
108
 
109
+ .stat-change {
110
+ font-size: 14px;
111
+ font-weight: 600;
112
  }
113
 
114
+ .stat-change.positive { color: var(--success); }
115
+ .stat-change.negative { color: var(--danger); }
 
 
116
 
117
+ /* Tabs */
118
+ .tabs {
119
+ display: flex;
120
+ gap: 10px;
121
+ margin-bottom: 30px;
122
+ flex-wrap: wrap;
123
  }
124
 
125
+ .tab-btn {
126
+ padding: 12px 24px;
127
+ background: rgba(255, 255, 255, 0.1);
128
+ border: none;
129
+ border-radius: 12px;
130
  color: white;
131
+ font-size: 16px;
132
+ font-weight: 600;
133
+ cursor: pointer;
134
+ transition: all 0.3s;
135
  }
136
 
137
+ .tab-btn:hover {
138
+ background: rgba(255, 255, 255, 0.2);
 
139
  }
140
 
141
+ .tab-btn.active {
142
+ background: rgba(255, 255, 255, 0.3);
 
 
 
143
  }
144
 
145
+ /* Content Sections */
146
+ .tab-content {
147
+ display: none;
 
 
 
148
  }
149
 
150
+ .tab-content.active {
151
+ display: block;
 
 
 
 
152
  }
153
 
154
+ /* Card Grid */
155
+ .card-grid {
156
  display: grid;
157
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
158
+ gap: 20px;
159
  }
160
 
161
+ .coin-card {
162
  background: rgba(255, 255, 255, 0.1);
163
+ backdrop-filter: blur(20px);
164
+ border-radius: 15px;
165
+ padding: 20px;
166
+ cursor: pointer;
167
+ transition: all 0.3s;
 
 
 
 
 
 
 
 
168
  }
169
 
170
+ .coin-card:hover {
171
+ transform: translateY(-5px);
172
+ box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
173
  }
174
 
175
+ .coin-header {
 
 
 
 
176
  display: flex;
177
  justify-content: space-between;
178
  align-items: center;
179
+ margin-bottom: 15px;
180
  }
181
 
182
+ .coin-symbol {
183
+ font-size: 24px;
184
+ font-weight: 800;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
 
187
+ .coin-rank {
 
 
 
 
 
188
  background: rgba(255, 255, 255, 0.2);
189
+ padding: 4px 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  border-radius: 10px;
 
 
 
 
191
  font-size: 12px;
192
  }
193
 
194
+ .coin-price {
195
+ font-size: 28px;
196
+ font-weight: 800;
197
+ margin-bottom: 10px;
 
198
  }
199
 
200
+ .coin-change {
201
+ font-size: 16px;
202
+ font-weight: 600;
203
  }
204
 
205
+ /* News Card */
206
+ .news-card {
207
+ background: rgba(255, 255, 255, 0.1);
208
+ backdrop-filter: blur(20px);
209
+ border-radius: 15px;
210
+ padding: 20px;
211
+ cursor: pointer;
212
+ transition: all 0.3s;
 
 
 
213
  }
214
 
215
+ .news-card:hover {
216
+ background: rgba(255, 255, 255, 0.15);
217
  }
218
 
219
+ .news-title {
220
+ font-size: 18px;
221
+ font-weight: 700;
222
+ margin-bottom: 10px;
 
 
 
223
  }
224
 
225
+ .news-meta {
226
  display: flex;
227
  justify-content: space-between;
228
+ font-size: 14px;
229
+ opacity: 0.8;
 
 
 
 
 
 
 
 
230
  }
231
 
232
+ /* Sentiment Gauge */
233
+ .sentiment-container {
234
+ background: rgba(255, 255, 255, 0.1);
235
+ backdrop-filter: blur(20px);
236
+ border-radius: 20px;
237
+ padding: 30px;
238
+ text-align: center;
239
  }
240
 
241
+ .sentiment-value {
242
+ font-size: 64px;
243
+ font-weight: 900;
244
+ margin: 20px 0;
 
245
  }
246
 
247
+ .sentiment-label {
248
+ font-size: 24px;
249
+ font-weight: 700;
250
+ margin-bottom: 20px;
 
 
251
  }
252
 
253
+ .gauge {
254
  width: 100%;
255
+ height: 40px;
256
+ background: linear-gradient(to right, #ef4444, #f59e0b, #10b981);
257
+ border-radius: 20px;
258
+ position: relative;
259
+ margin: 30px 0;
260
  }
261
 
262
+ .gauge-pointer {
263
+ position: absolute;
264
+ width: 20px;
265
+ height: 20px;
266
+ background: white;
267
+ border-radius: 50%;
268
+ top: 10px;
269
+ transform: translateX(-50%);
270
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
271
  }
272
 
273
+ /* Chart Container */
274
+ .chart-container {
275
  background: rgba(255, 255, 255, 0.1);
276
+ backdrop-filter: blur(20px);
277
+ border-radius: 20px;
278
+ padding: 30px;
279
+ margin-top: 30px;
 
 
280
  }
281
 
282
+ .chart-title {
283
+ font-size: 20px;
284
+ font-weight: 700;
285
+ margin-bottom: 20px;
286
  }
287
 
288
+ /* Loading */
289
+ .loading {
290
+ text-align: center;
291
+ padding: 40px;
292
+ font-size: 18px;
 
 
 
 
 
 
 
 
293
  }
294
 
295
+ /* Responsive */
296
+ @media (max-width: 768px) {
297
+ .stats-grid {
298
+ grid-template-columns: 1fr;
299
+ }
300
+
301
+ .card-grid {
302
+ grid-template-columns: 1fr;
303
+ }
304
  }
305
  </style>
306
  </head>
307
  <body>
308
  <div class="container">
309
+ <!-- Header -->
310
+ <div class="header">
311
+ <div class="header-title">
312
  <span>🚀</span>
313
+ <span>داشبورد هوشمند کریپتو</span>
314
+ <span class="status-badge">
315
+ <span class="status-dot"></span>
316
+ آنلاین
317
+ </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  </div>
319
  </div>
320
 
321
+ <!-- Market Stats -->
322
+ <div class="stats-grid" id="marketStats">
323
+ <div class="stat-card">
324
+ <div class="stat-label">کل ارزش بازار</div>
325
+ <div class="stat-value" id="totalMarketCap">$0</div>
326
+ <div class="stat-change positive" id="marketCapChange">+0%</div>
327
+ </div>
328
+ <div class="stat-card">
329
+ <div class="stat-label">حجم معاملات 24 ساعت</div>
330
+ <div class="stat-value" id="totalVolume">$0</div>
331
+ </div>
332
+ <div class="stat-card">
333
+ <div class="stat-label">تسلط بیت‌کوین</div>
334
+ <div class="stat-value" id="btcDominance">0%</div>
 
 
 
 
 
 
 
335
  </div>
336
+ <div class="stat-card">
337
+ <div class="stat-label">تعداد ارزها</div>
338
+ <div class="stat-value" id="activeCryptos">0</div>
339
+ </div>
340
+ </div>
341
 
342
+ <!-- Tabs -->
343
+ <div class="tabs">
344
+ <button class="tab-btn active" onclick="switchTab('trending')">🔥 ترندها</button>
345
+ <button class="tab-btn" onclick="switchTab('top')">💎 برترین‌ها</button>
346
+ <button class="tab-btn" onclick="switchTab('news')">📰 اخبار</button>
347
+ <button class="tab-btn" onclick="switchTab('sentiment')">📊 احساسات</button>
348
+ <button class="tab-btn" onclick="switchTab('blockchain')">⛓️ بلاکچین</button>
349
+ </div>
350
+
351
+ <!-- Trending Tab -->
352
+ <div id="trending" class="tab-content active">
353
+ <h2 style="margin-bottom: 20px;">🔥 ارزهای ترند</h2>
354
+ <div class="card-grid" id="trendingGrid">
355
+ <div class="loading">در حال بارگذاری...</div>
356
  </div>
357
  </div>
358
 
359
+ <!-- Top Coins Tab -->
360
+ <div id="top" class="tab-content">
361
+ <h2 style="margin-bottom: 20px;">💎 برترین ارزها</h2>
362
+ <div class="card-grid" id="topGrid">
363
+ <div class="loading">در حال بارگذاری...</div>
364
  </div>
365
  </div>
 
366
 
367
+ <!-- News Tab -->
368
+ <div id="news" class="tab-content">
369
+ <h2 style="margin-bottom: 20px;">📰 آخرین اخبار</h2>
370
+ <div class="card-grid" id="newsGrid">
371
+ <div class="loading">در حال بارگذاری...</div>
 
372
  </div>
373
+ </div>
374
+
375
+ <!-- Sentiment Tab -->
376
+ <div id="sentiment" class="tab-content">
377
+ <h2 style="margin-bottom: 20px;">📊 احساسات بازار</h2>
378
+ <div class="sentiment-container">
379
+ <div class="sentiment-label">شاخص ترس و طمع</div>
380
+ <div class="sentiment-value" id="sentimentValue">50</div>
381
+ <div class="sentiment-label" id="sentimentLabel">خنثی</div>
382
+ <div class="gauge">
383
+ <div class="gauge-pointer" id="gaugePointer" style="left: 50%;"></div>
384
+ </div>
385
  </div>
386
+ <div class="chart-container">
387
+ <div class="chart-title">تاریخچه 7 روز اخیر</div>
388
+ <canvas id="sentimentChart" height="100"></canvas>
389
  </div>
390
+ </div>
391
+
392
+ <!-- Blockchain Tab -->
393
+ <div id="blockchain" class="tab-content">
394
+ <h2 style="margin-bottom: 20px;">⛓️ آمار بلاکچین</h2>
395
+ <div class="stats-grid">
396
+ <div class="stat-card">
397
+ <div class="stat-label">قیمت گس اتریوم</div>
398
+ <div class="stat-value" id="ethGas">0 Gwei</div>
399
+ </div>
400
+ <div class="stat-card">
401
+ <div class="stat-label">آخرین بلاک اتریوم</div>
402
+ <div class="stat-value" id="ethBlock">0</div>
403
+ </div>
404
+ <div class="stat-card">
405
+ <div class="stat-label">قیمت گس BSC</div>
406
+ <div class="stat-value" id="bscGas">0 Gwei</div>
407
+ </div>
408
+ <div class="stat-card">
409
+ <div class="stat-label">آخرین بلاک BSC</div>
410
+ <div class="stat-value" id="bscBlock">0</div>
411
+ </div>
412
  </div>
 
413
  </div>
414
  </div>
415
 
 
 
 
416
  <script>
417
+ let currentTab = 'trending';
418
+
419
+ // Switch tabs
420
+ function switchTab(tabName) {
421
+ currentTab = tabName;
422
+
423
+ // Update active tab button
424
+ document.querySelectorAll('.tab-btn').forEach(btn => {
425
+ btn.classList.remove('active');
426
+ });
427
+ event.target.classList.add('active');
428
+
429
+ // Update active content
430
+ document.querySelectorAll('.tab-content').forEach(content => {
431
+ content.classList.remove('active');
432
+ });
433
+ document.getElementById(tabName).classList.add('active');
434
+
435
+ // Load data for the tab
436
+ loadTabData(tabName);
437
+ }
438
+
439
+ // Load tab data
440
+ async function loadTabData(tabName) {
441
+ switch(tabName) {
442
+ case 'trending':
443
+ await loadTrending();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  break;
445
+ case 'top':
446
+ await loadTopCoins();
447
  break;
448
+ case 'news':
449
+ await loadNews();
450
  break;
451
+ case 'sentiment':
452
+ await loadSentiment();
453
+ break;
454
+ case 'blockchain':
455
+ await loadBlockchain();
456
  break;
457
  }
458
  }
459
 
460
+ // Load market overview
461
+ async function loadMarketOverview() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
  try {
463
+ const response = await fetch('/api/crypto/market-overview');
 
 
 
 
 
464
  const data = await response.json();
465
+
466
+ document.getElementById('totalMarketCap').textContent = formatCurrency(data.total_market_cap);
467
+ document.getElementById('totalVolume').textContent = formatCurrency(data.total_volume_24h);
468
+ document.getElementById('btcDominance').textContent = data.btc_dominance.toFixed(1) + '%';
469
+ document.getElementById('activeCryptos').textContent = data.active_cryptocurrencies.toLocaleString();
470
+
471
+ const changeElement = document.getElementById('marketCapChange');
472
+ const change = data.market_cap_change_24h;
473
+ changeElement.textContent = (change > 0 ? '+' : '') + change.toFixed(2) + '%';
474
+ changeElement.className = 'stat-change ' + (change > 0 ? 'positive' : 'negative');
475
  } catch (error) {
476
+ console.error('Error loading market overview:', error);
 
477
  }
478
  }
479
 
480
+ // Load trending coins
481
+ async function loadTrending() {
482
  try {
483
+ const response = await fetch('/api/crypto/prices/trending?limit=12');
484
+ const coins = await response.json();
485
+
486
+ const grid = document.getElementById('trendingGrid');
487
+ grid.innerHTML = coins.map(coin => createCoinCard(coin)).join('');
 
 
 
 
 
 
 
 
488
  } catch (error) {
489
+ console.error('Error loading trending:', error);
 
490
  }
491
  }
492
 
493
+ // Load top coins
494
+ async function loadTopCoins() {
495
  try {
496
+ const response = await fetch('/api/crypto/prices/top?limit=20');
497
+ const coins = await response.json();
498
+
499
+ const grid = document.getElementById('topGrid');
500
+ grid.innerHTML = coins.map(coin => createCoinCard(coin)).join('');
 
 
 
501
  } catch (error) {
502
+ console.error('Error loading top coins:', error);
 
503
  }
504
  }
505
 
506
+ // Load news
507
+ async function loadNews() {
508
  try {
509
+ const response = await fetch('/api/crypto/news/latest?limit=12');
510
+ const news = await response.json();
511
+
512
+ const grid = document.getElementById('newsGrid');
513
+ grid.innerHTML = news.map(article => `
514
+ <div class="news-card" onclick="window.open('${article.url}', '_blank')">
515
+ <div class="news-title">${article.title}</div>
516
+ <div class="news-meta">
517
+ <span>${article.source}</span>
518
+ <span>${formatTime(article.published_at)}</span>
519
+ </div>
520
+ </div>
521
+ `).join('');
522
  } catch (error) {
523
+ console.error('Error loading news:', error);
 
524
  }
525
  }
526
 
527
+ // Load sentiment
528
+ async function loadSentiment() {
 
 
529
  try {
530
+ const [current, history] = await Promise.all([
531
+ fetch('/api/crypto/sentiment/current').then(r => r.json()),
532
+ fetch('/api/crypto/sentiment/history?hours=168').then(r => r.json())
533
+ ]);
534
+
535
+ // Update current sentiment
536
+ document.getElementById('sentimentValue').textContent = current.fear_greed_index;
537
+ document.getElementById('sentimentLabel').textContent = current.classification;
538
+
539
+ // Update gauge pointer
540
+ const pointer = document.getElementById('gaugePointer');
541
+ pointer.style.left = current.fear_greed_index + '%';
542
+
543
+ // Update chart
544
+ if (history.history && history.history.length > 0) {
545
+ updateSentimentChart(history.history);
546
  }
 
 
547
  } catch (error) {
548
+ console.error('Error loading sentiment:', error);
 
549
  }
550
  }
551
 
552
+ // Load blockchain stats
553
+ async function loadBlockchain() {
 
554
  try {
555
+ const [gas, stats] = await Promise.all([
556
+ fetch('/api/crypto/blockchain/gas').then(r => r.json()),
557
+ fetch('/api/crypto/blockchain/stats').then(r => r.json())
558
+ ]);
559
+
560
+ document.getElementById('ethGas').textContent = gas.ethereum.gas_price_gwei + ' Gwei';
561
+ document.getElementById('ethBlock').textContent = stats.ethereum.latest_block.toLocaleString();
562
+ document.getElementById('bscGas').textContent = gas.bsc.gas_price_gwei + ' Gwei';
563
+ document.getElementById('bscBlock').textContent = stats.bsc.latest_block.toLocaleString();
564
  } catch (error) {
565
+ console.error('Error loading blockchain:', error);
 
566
  }
567
  }
568
 
569
+ // Create coin card HTML
570
+ function createCoinCard(coin) {
571
+ const changeClass = coin.price_change_24h >= 0 ? 'positive' : 'negative';
572
+ const changeSymbol = coin.price_change_24h >= 0 ? '+' : '';
573
+
574
+ return `
575
+ <div class="coin-card">
576
+ <div class="coin-header">
577
+ <div class="coin-symbol">${coin.symbol}</div>
578
+ <div class="coin-rank">#${coin.rank}</div>
579
+ </div>
580
+ <div style="font-size: 14px; opacity: 0.8; margin-bottom: 10px;">${coin.name}</div>
581
+ <div class="coin-price">$${formatNumber(coin.price)}</div>
582
+ <div class="coin-change ${changeClass}">
583
+ ${changeSymbol}${coin.price_change_24h.toFixed(2)}%
584
+ </div>
585
+ </div>
586
+ `;
587
+ }
588
+
589
+ // Update sentiment chart
590
+ function updateSentimentChart(data) {
591
+ const ctx = document.getElementById('sentimentChart');
592
+
593
+ new Chart(ctx, {
594
+ type: 'line',
595
+ data: {
596
+ labels: data.map(d => new Date(d.timestamp).toLocaleDateString('fa-IR')),
597
+ datasets: [{
598
+ label: 'شاخص ترس و طمع',
599
+ data: data.map(d => d.value),
600
+ borderColor: 'rgba(255, 255, 255, 0.8)',
601
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
602
+ tension: 0.4,
603
+ fill: true,
604
+ borderWidth: 3
605
+ }]
606
+ },
607
+ options: {
608
+ responsive: true,
609
+ plugins: {
610
+ legend: { display: false }
611
+ },
612
+ scales: {
613
+ y: {
614
+ beginAtZero: true,
615
+ max: 100,
616
+ ticks: { color: 'rgba(255, 255, 255, 0.8)' },
617
+ grid: { color: 'rgba(255, 255, 255, 0.1)' }
618
+ },
619
+ x: {
620
+ ticks: { color: 'rgba(255, 255, 255, 0.8)' },
621
+ grid: { color: 'rgba(255, 255, 255, 0.1)' }
622
+ }
623
+ }
624
+ }
625
+ });
626
  }
627
 
628
+ // Helper functions
629
+ function formatNumber(num) {
630
+ if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
631
+ if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
632
+ if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
633
+ return num.toFixed(2);
 
 
 
 
 
 
 
 
 
 
 
634
  }
635
 
636
+ function formatCurrency(num) {
637
+ return '$' + formatNumber(num);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  }
639
 
640
+ function formatTime(timestamp) {
641
+ const date = new Date(timestamp);
642
+ const now = new Date();
643
+ const diff = Math.floor((now - date) / 1000);
644
+
645
+ if (diff < 60) return 'همین الان';
646
+ if (diff < 3600) return Math.floor(diff / 60) + ' دقیقه پیش';
647
+ if (diff < 86400) return Math.floor(diff / 3600) + ' ساعت پیش';
648
+ return Math.floor(diff / 86400) + ' روز پیش';
 
 
 
 
 
 
 
 
 
 
 
649
  }
650
 
651
+ // Initialize
652
+ document.addEventListener('DOMContentLoaded', () => {
653
+ loadMarketOverview();
654
+ loadTrending();
655
+
656
+ // Auto refresh every 30 seconds
657
+ setInterval(() => {
658
+ loadMarketOverview();
659
+ loadTabData(currentTab);
660
+ }, 30000);
661
  });
662
  </script>
663
  </body>
664
+ </html>