Really-amin commited on
Commit
968720f
Β·
verified Β·
1 Parent(s): d362646

Update static/pages/dashboard/dashboard.js

Browse files
Files changed (1) hide show
  1. static/pages/dashboard/dashboard.js +1347 -0
static/pages/dashboard/dashboard.js ADDED
@@ -0,0 +1,1347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Dashboard Page - Ultra Modern Design with Enhanced Visuals
3
+ * @version 3.0.0
4
+ */
5
+
6
+ import { formatNumber, formatCurrency, formatPercentage } from '../../shared/js/utils/formatters.js';
7
+ import { apiClient } from '../../shared/js/api-client.js';
8
+ import logger from '../../shared/js/utils/logger.js';
9
+
10
+ class DashboardPage {
11
+ constructor() {
12
+ this.charts = {};
13
+ this.marketData = [];
14
+ this.watchlist = [];
15
+ this.priceAlerts = [];
16
+ this.newsCache = [];
17
+ this.updateInterval = null;
18
+ this.isLoading = false;
19
+ this.consecutiveFailures = 0;
20
+ this.isOffline = false;
21
+ this.expandedNews = new Set();
22
+
23
+ this.config = {
24
+ refreshInterval: 30000,
25
+ maxWatchlistItems: 8,
26
+ maxNewsItems: 6
27
+ };
28
+
29
+ this.loadPersistedData();
30
+ }
31
+
32
+ async init() {
33
+ try {
34
+ logger.info('Dashboard', 'Initializing enhanced dashboard...');
35
+
36
+ // Show loading state
37
+ this.showLoadingState();
38
+
39
+ // Defer Chart.js loading until after initial render
40
+ this.injectEnhancedLayout();
41
+ this.bindEvents();
42
+
43
+ // Add smooth fade-in delay for better UX
44
+ await new Promise(resolve => setTimeout(resolve, 300));
45
+
46
+ // Load data first (critical), then load Chart.js lazily
47
+ await this.loadAllData();
48
+
49
+ // Remove loading state with fade
50
+ this.hideLoadingState();
51
+
52
+ // Load Chart.js only when charts are needed (lazy)
53
+ if (window.requestIdleCallback) {
54
+ window.requestIdleCallback(() => this.loadChartJS(), { timeout: 3000 });
55
+ } else {
56
+ setTimeout(() => this.loadChartJS(), 500);
57
+ }
58
+ this.setupAutoRefresh();
59
+
60
+ // Show rating prompt after a brief delay
61
+ setTimeout(() => this.showRatingWidget(), 5000);
62
+
63
+ this.showToast('Dashboard ready', 'success');
64
+ } catch (error) {
65
+ logger.error('Dashboard', 'Init error:', error);
66
+ this.showToast('Failed to load dashboard', 'error');
67
+ }
68
+ }
69
+
70
+ loadPersistedData() {
71
+ try {
72
+ const savedWatchlist = localStorage.getItem('crypto_watchlist');
73
+ this.watchlist = savedWatchlist ? JSON.parse(savedWatchlist) : ['bitcoin', 'ethereum', 'solana', 'cardano', 'ripple'];
74
+ const savedAlerts = localStorage.getItem('crypto_price_alerts');
75
+ this.priceAlerts = savedAlerts ? JSON.parse(savedAlerts) : [];
76
+ } catch (error) {
77
+ logger.error('Dashboard', 'Error loading persisted data:', error);
78
+ }
79
+ }
80
+
81
+ savePersistedData() {
82
+ try {
83
+ localStorage.setItem('crypto_watchlist', JSON.stringify(this.watchlist));
84
+ localStorage.setItem('crypto_price_alerts', JSON.stringify(this.priceAlerts));
85
+ } catch (error) {
86
+ logger.error('Dashboard', 'Error saving:', error);
87
+ }
88
+ }
89
+
90
+ destroy() {
91
+ if (this.updateInterval) clearInterval(this.updateInterval);
92
+ Object.values(this.charts).forEach(chart => chart?.destroy());
93
+ this.charts = {};
94
+ this.savePersistedData();
95
+ }
96
+
97
+ showLoadingState() {
98
+ const pageContent = document.querySelector('.page-content');
99
+ if (!pageContent) return;
100
+
101
+ // Add loading skeleton overlay
102
+ const loadingOverlay = document.createElement('div');
103
+ loadingOverlay.id = 'dashboard-loading';
104
+ loadingOverlay.className = 'dashboard-loading-overlay';
105
+ loadingOverlay.innerHTML = `
106
+ <div class="loading-content">
107
+ <div class="loading-spinner"></div>
108
+ <p class="loading-text">Loading Dashboard...</p>
109
+ </div>
110
+ `;
111
+ pageContent.appendChild(loadingOverlay);
112
+ }
113
+
114
+ hideLoadingState() {
115
+ const loadingOverlay = document.getElementById('dashboard-loading');
116
+ if (loadingOverlay) {
117
+ loadingOverlay.classList.add('fade-out');
118
+ setTimeout(() => loadingOverlay.remove(), 400);
119
+ }
120
+ }
121
+
122
+ showRatingWidget() {
123
+ // Check if user has already rated this session
124
+ const hasRated = sessionStorage.getItem('dashboard_rated');
125
+ if (hasRated) return;
126
+
127
+ const ratingWidget = document.createElement('div');
128
+ ratingWidget.id = 'rating-widget';
129
+ ratingWidget.className = 'rating-widget';
130
+ ratingWidget.innerHTML = `
131
+ <div class="rating-content">
132
+ <button class="rating-close" onclick="this.closest('.rating-widget').remove()">&times;</button>
133
+ <h4>How's your experience?</h4>
134
+ <p>Rate the Crypto Monitor Dashboard</p>
135
+ <div class="rating-stars">
136
+ <button class="star-btn" data-rating="1">β˜…</button>
137
+ <button class="star-btn" data-rating="2">β˜…</button>
138
+ <button class="star-btn" data-rating="3">β˜…</button>
139
+ <button class="star-btn" data-rating="4">β˜…</button>
140
+ <button class="star-btn" data-rating="5">β˜…</button>
141
+ </div>
142
+ <p class="rating-feedback" style="display:none; margin-top:10px; color: var(--success); font-weight:600;"></p>
143
+ </div>
144
+ `;
145
+
146
+ document.body.appendChild(ratingWidget);
147
+
148
+ // Add rating interaction
149
+ const stars = ratingWidget.querySelectorAll('.star-btn');
150
+ const feedback = ratingWidget.querySelector('.rating-feedback');
151
+
152
+ stars.forEach((star, index) => {
153
+ star.addEventListener('mouseenter', () => {
154
+ stars.forEach((s, i) => {
155
+ s.classList.toggle('active', i <= index);
156
+ });
157
+ });
158
+
159
+ star.addEventListener('click', () => {
160
+ const rating = parseInt(star.dataset.rating);
161
+ sessionStorage.setItem('dashboard_rated', rating);
162
+
163
+ feedback.textContent = `Thank you for rating ${rating} stars!`;
164
+ feedback.style.display = 'block';
165
+
166
+ setTimeout(() => {
167
+ ratingWidget.classList.add('fade-out');
168
+ setTimeout(() => ratingWidget.remove(), 400);
169
+ }, 2000);
170
+ });
171
+ });
172
+
173
+ ratingWidget.addEventListener('mouseleave', () => {
174
+ stars.forEach(s => s.classList.remove('active'));
175
+ });
176
+
177
+ // Auto-hide after 20 seconds
178
+ setTimeout(() => {
179
+ if (ratingWidget.parentNode) {
180
+ ratingWidget.classList.add('fade-out');
181
+ setTimeout(() => ratingWidget.remove(), 400);
182
+ }
183
+ }, 20000);
184
+ }
185
+
186
+ async loadChartJS() {
187
+ if (window.Chart) {
188
+ console.log('[Dashboard] Chart.js already loaded');
189
+ return;
190
+ }
191
+
192
+ console.log('[Dashboard] Loading Chart.js...');
193
+ // Lazy load Chart.js only when needed (when charts are about to be rendered)
194
+ return new Promise((resolve, reject) => {
195
+ const script = document.createElement('script');
196
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js';
197
+ script.async = true;
198
+ script.defer = true;
199
+ script.crossOrigin = 'anonymous';
200
+ script.onload = () => {
201
+ console.log('[Dashboard] Chart.js loaded successfully');
202
+ // Force render charts after Chart.js loads
203
+ setTimeout(() => {
204
+ this.renderAllCharts();
205
+ }, 100);
206
+ resolve();
207
+ };
208
+ script.onerror = (e) => {
209
+ console.error('[Dashboard] Chart.js load failed:', e);
210
+ reject(e);
211
+ };
212
+ document.head.appendChild(script);
213
+ });
214
+ }
215
+
216
+ renderAllCharts() {
217
+ console.log('[Dashboard] Charts will be rendered when data is loaded...');
218
+
219
+ console.log('[Dashboard] Charts rendered');
220
+ }
221
+
222
+ injectEnhancedLayout() {
223
+ const pageContent = document.querySelector('.page-content');
224
+ if (!pageContent) return;
225
+
226
+ // Create enhanced layout
227
+ pageContent.innerHTML = `
228
+ <!-- Live Ticker Bar -->
229
+ <div class="ticker-bar" id="ticker-bar">
230
+ <div class="ticker-track" id="ticker-track"></div>
231
+ </div>
232
+
233
+ <!-- Hero Stats Section -->
234
+ <section class="hero-stats" id="hero-stats">
235
+ <div class="hero-stat-card primary">
236
+ <div class="hero-stat-bg"></div>
237
+ <div class="hero-stat-content">
238
+ <div class="hero-stat-icon">
239
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
240
+ </div>
241
+ <div class="hero-stat-info">
242
+ <span class="hero-stat-label">Total Resources</span>
243
+ <span class="hero-stat-value" id="stat-resources">--</span>
244
+ <div class="hero-stat-trend positive">
245
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg>
246
+ <span>Active</span>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ <div class="hero-stat-progress">
251
+ <div class="progress-bar" style="width: 100%"></div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="hero-stat-card accent">
256
+ <div class="hero-stat-bg"></div>
257
+ <div class="hero-stat-content">
258
+ <div class="hero-stat-icon">
259
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>
260
+ </div>
261
+ <div class="hero-stat-info">
262
+ <span class="hero-stat-label">API Keys</span>
263
+ <span class="hero-stat-value" id="stat-apikeys">--</span>
264
+ <div class="hero-stat-trend">
265
+ <span class="badge badge-info">Configured</span>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+
271
+ <div class="hero-stat-card success">
272
+ <div class="hero-stat-bg"></div>
273
+ <div class="hero-stat-content">
274
+ <div class="hero-stat-icon">
275
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3"/></svg>
276
+ </div>
277
+ <div class="hero-stat-info">
278
+ <span class="hero-stat-label">AI Models</span>
279
+ <span class="hero-stat-value" id="stat-models">--</span>
280
+ <div class="hero-stat-trend">
281
+ <span class="badge badge-success">Ready</span>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </div>
286
+
287
+ <div class="hero-stat-card warning">
288
+ <div class="hero-stat-bg"></div>
289
+ <div class="hero-stat-content">
290
+ <div class="hero-stat-icon">
291
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>
292
+ </div>
293
+ <div class="hero-stat-info">
294
+ <span class="hero-stat-label">Providers</span>
295
+ <span class="hero-stat-value" id="stat-providers">--</span>
296
+ <div class="hero-stat-trend positive">
297
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg>
298
+ <span>Online</span>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </section>
304
+
305
+ <!-- Main Dashboard Grid -->
306
+ <div class="dashboard-grid">
307
+ <!-- Left Column -->
308
+ <div class="dashboard-col-main">
309
+ <!-- Market Overview Card -->
310
+ <div class="glass-card market-card">
311
+ <div class="card-header">
312
+ <div class="card-title">
313
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/></svg>
314
+ <h2>Market Overview</h2>
315
+ </div>
316
+ <div class="card-controls">
317
+ <input type="text" class="search-pill" id="market-search" placeholder="Search...">
318
+ <select class="select-pill" id="market-sort">
319
+ <option value="rank">Rank</option>
320
+ <option value="price">Price</option>
321
+ <option value="change">24h %</option>
322
+ </select>
323
+ </div>
324
+ </div>
325
+ <div class="card-body" id="market-table-container">
326
+ <div class="loading-pulse">Loading market data...</div>
327
+ </div>
328
+ </div>
329
+
330
+ <!-- Charts Row -->
331
+ <div class="charts-row">
332
+ <!-- Sentiment Chart -->
333
+ <div class="glass-card chart-card">
334
+ <div class="card-header">
335
+ <div class="card-title">
336
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
337
+ <h2>Fear & Greed Index</h2>
338
+ </div>
339
+ <div class="timeframe-pills" id="sentiment-timeframe">
340
+ <button class="pill active" data-tf="1D">1D</button>
341
+ <button class="pill" data-tf="7D">7D</button>
342
+ <button class="pill" data-tf="30D">30D</button>
343
+ </div>
344
+ </div>
345
+ <div class="chart-wrapper">
346
+ <canvas id="sentiment-chart"></canvas>
347
+ </div>
348
+ <div class="sentiment-gauge" id="sentiment-gauge"></div>
349
+ </div>
350
+
351
+ <!-- Resources Chart -->
352
+ <div class="glass-card chart-card">
353
+ <div class="card-header">
354
+ <div class="card-title">
355
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>
356
+ <h2>API Resources</h2>
357
+ </div>
358
+ </div>
359
+ <div class="chart-wrapper donut-wrapper">
360
+ <canvas id="categories-chart"></canvas>
361
+ <div class="donut-center" id="donut-center">
362
+ <span class="donut-value">--</span>
363
+ <span class="donut-label">Total</span>
364
+ </div>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <!-- Right Column - Sidebar -->
371
+ <div class="dashboard-col-side">
372
+ <!-- News Accordion Card -->
373
+ <div class="glass-card news-card">
374
+ <div class="card-header compact">
375
+ <div class="card-title">
376
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></svg>
377
+ <h3>Latest News</h3>
378
+ </div>
379
+ <a href="/static/pages/news/index.html" class="btn-ghost">View All</a>
380
+ </div>
381
+ <div class="news-accordion" id="news-accordion"></div>
382
+ </div>
383
+
384
+ <!-- Price Alerts Card -->
385
+ <div class="glass-card alerts-card">
386
+ <div class="card-header compact">
387
+ <div class="card-title">
388
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
389
+ <h3>Price Alerts</h3>
390
+ </div>
391
+ <button class="btn-ghost" id="alert-add" title="Add alert">+</button>
392
+ </div>
393
+ <div class="alerts-list" id="alerts-list"></div>
394
+ </div>
395
+
396
+ <!-- Quick Stats -->
397
+ <div class="glass-card mini-stats-card">
398
+ <div class="mini-stat">
399
+ <span class="mini-stat-label">Response Time</span>
400
+ <span class="mini-stat-value" id="stat-response">-- ms</span>
401
+ </div>
402
+ <div class="mini-stat">
403
+ <span class="mini-stat-label">Cache Hit</span>
404
+ <span class="mini-stat-value" id="stat-cache">-- %</span>
405
+ </div>
406
+ <div class="mini-stat">
407
+ <span class="mini-stat-label">Sessions</span>
408
+ <span class="mini-stat-value" id="stat-sessions">--</span>
409
+ </div>
410
+ </div>
411
+ </div>
412
+ </div>
413
+ `;
414
+ }
415
+
416
+ bindEvents() {
417
+ // Refresh button
418
+ document.getElementById('refresh-btn')?.addEventListener('click', () => {
419
+ this.showToast('Refreshing...', 'info');
420
+ this.loadAllData();
421
+ });
422
+
423
+ // Market search
424
+ document.getElementById('market-search')?.addEventListener('input', (e) => {
425
+ this.filterMarketTable(e.target.value);
426
+ });
427
+
428
+ // Market sort
429
+ document.getElementById('market-sort')?.addEventListener('change', (e) => {
430
+ this.sortMarketData(e.target.value);
431
+ });
432
+
433
+ // Sentiment timeframe
434
+ document.querySelectorAll('#sentiment-timeframe .pill').forEach(btn => {
435
+ btn.addEventListener('click', () => {
436
+ document.querySelectorAll('#sentiment-timeframe .pill').forEach(b => b.classList.remove('active'));
437
+ btn.classList.add('active');
438
+ this.updateSentimentTimeframe(btn.dataset.tf);
439
+ });
440
+ });
441
+
442
+ // Watchlist removed - not needed
443
+
444
+ // Alert add
445
+ document.getElementById('alert-add')?.addEventListener('click', () => this.showAddAlertModal());
446
+
447
+ // Visibility change
448
+ document.addEventListener('visibilitychange', () => {
449
+ if (!document.hidden && !this.isOffline) this.loadAllData();
450
+ });
451
+ }
452
+
453
+ setupAutoRefresh() {
454
+ this.updateInterval = setInterval(() => {
455
+ if (!this.isOffline && !document.hidden && !this.isLoading) {
456
+ this.loadAllData();
457
+ }
458
+ }, this.config.refreshInterval);
459
+ }
460
+
461
+ async loadAllData() {
462
+ if (this.isLoading) return;
463
+ this.isLoading = true;
464
+
465
+ try {
466
+ // Show loading indicator
467
+ const marketContainer = document.getElementById('market-table-container');
468
+ if (marketContainer) {
469
+ marketContainer.innerHTML = '<div class="loading-pulse">Loading market data...</div>';
470
+ }
471
+
472
+ const [stats, market, sentiment, resources, news] = await Promise.allSettled([
473
+ this.fetchStats(),
474
+ this.fetchMarket(),
475
+ this.fetchSentiment(),
476
+ this.fetchResources(),
477
+ this.fetchNews()
478
+ ]);
479
+
480
+ // Only render if we have real data
481
+ if (stats.status === 'fulfilled' && stats.value) {
482
+ this.renderStats(stats.value);
483
+ } else {
484
+ console.warn('[Dashboard] Stats unavailable');
485
+ this.renderStats({ total_resources: 0, api_keys: 0, models_loaded: 0, active_providers: 0 });
486
+ }
487
+
488
+ if (market.status === 'fulfilled' && market.value && market.value.length > 0) {
489
+ this.renderMarketTable(market.value);
490
+ this.renderTicker(market.value);
491
+ } else {
492
+ console.warn('[Dashboard] Market data unavailable');
493
+ if (marketContainer) {
494
+ marketContainer.innerHTML = '<div class="empty-state"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin: 0 auto 12px; opacity: 0.3;"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg><p>No market data available</p><p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">Please check your connection</p></div>';
495
+ }
496
+ }
497
+
498
+ if (sentiment.status === 'fulfilled' && sentiment.value) {
499
+ this.renderSentimentChart(sentiment.value);
500
+ } else {
501
+ console.warn('[Dashboard] Sentiment data unavailable');
502
+ }
503
+
504
+ if (resources.status === 'fulfilled' && resources.value) {
505
+ this.renderResourcesChart(resources.value);
506
+ } else {
507
+ console.warn('[Dashboard] Resources data unavailable');
508
+ }
509
+
510
+ if (news.status === 'fulfilled' && news.value && news.value.length > 0) {
511
+ this.renderNewsAccordion(news.value);
512
+ } else {
513
+ console.warn('[Dashboard] News unavailable');
514
+ }
515
+
516
+ this.renderAlerts();
517
+ this.renderMiniStats();
518
+ this.updateTimestamp();
519
+
520
+ // Reset failure counter on success
521
+ this.consecutiveFailures = 0;
522
+ this.isOffline = false;
523
+
524
+ } catch (error) {
525
+ logger.error('Dashboard', 'Load error:', error);
526
+ this.consecutiveFailures++;
527
+ if (this.consecutiveFailures >= 3) {
528
+ this.isOffline = true;
529
+ this.showToast('Connection lost. Please check your internet.', 'error');
530
+ } else {
531
+ this.showToast('Failed to load some data', 'warning');
532
+ }
533
+ } finally {
534
+ this.isLoading = false;
535
+ }
536
+ }
537
+
538
+ // ============================================================================
539
+ // FETCH METHODS
540
+ // ============================================================================
541
+
542
+ async fetchStats() {
543
+ try {
544
+ const [res1, res2] = await Promise.allSettled([
545
+ apiClient.fetch('/api/resources/summary', {}, 15000).then(r => r.ok ? r.json() : null),
546
+ apiClient.fetch('/api/models/status', {}, 10000).then(r => r.ok ? r.json() : null)
547
+ ]);
548
+
549
+ const data = res1.value?.summary || res1.value || {};
550
+ const models = res2.value || {};
551
+
552
+ return {
553
+ total_resources: data.total_resources || 0,
554
+ api_keys: data.total_api_keys || 0,
555
+ models_loaded: models.models_loaded || data.models_available || 0,
556
+ active_providers: data.total_resources || 0
557
+ };
558
+ } catch (error) {
559
+ console.error('[Dashboard] Stats fetch failed:', error);
560
+ return null;
561
+ }
562
+ }
563
+
564
+ async fetchMarket() {
565
+ try {
566
+ // Try backend API first
567
+ try {
568
+ const response = await apiClient.fetch('/api/market?limit=50', {}, 10000);
569
+ if (response.ok) {
570
+ const data = await response.json();
571
+ const markets = data.markets || data.coins || data.data || data;
572
+ if (Array.isArray(markets) && markets.length > 0) {
573
+ this.marketData = markets;
574
+ console.log('[Dashboard] Market data loaded from backend:', this.marketData.length, 'coins');
575
+ return this.marketData;
576
+ }
577
+ }
578
+ } catch (e) {
579
+ console.warn('[Dashboard] Backend API unavailable, trying CoinGecko');
580
+ }
581
+
582
+ // Fallback to CoinGecko direct API
583
+ const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1&sparkline=true&price_change_percentage=24h');
584
+
585
+ if (!response.ok) throw new Error('CoinGecko API failed');
586
+
587
+ const data = await response.json();
588
+ this.marketData = data || [];
589
+
590
+ console.log('[Dashboard] Market data loaded from CoinGecko:', this.marketData.length, 'coins');
591
+ return this.marketData;
592
+ } catch (error) {
593
+ console.error('[Dashboard] Market fetch failed:', error.message);
594
+ return [];
595
+ }
596
+ }
597
+
598
+ async fetchSentiment() {
599
+ try {
600
+ // Use Fear & Greed Index direct API
601
+ const response = await fetch('https://api.alternative.me/fng/');
602
+ if (!response.ok) throw new Error('Fear & Greed API failed');
603
+
604
+ const data = await response.json();
605
+ const val = parseInt(data.data?.[0]?.value || 50);
606
+
607
+ return {
608
+ fear_greed_index: val,
609
+ sentiment: val > 50 ? 'greed' : 'fear'
610
+ };
611
+ } catch (error) {
612
+ console.error('[Dashboard] Sentiment fetch failed:', error);
613
+ return { fear_greed_index: 50, sentiment: 'neutral' };
614
+ }
615
+ }
616
+
617
+ async fetchResources() {
618
+ try {
619
+ const response = await apiClient.fetch('/api/resources/stats', {}, 15000);
620
+ if (!response.ok) throw new Error();
621
+ const data = await response.json();
622
+ const stats = data.data || data;
623
+
624
+ return {
625
+ categories: {
626
+ 'Market': stats.categories?.market_data?.total || 13,
627
+ 'News': stats.categories?.news?.total || 10,
628
+ 'Sentiment': stats.categories?.sentiment?.total || 6,
629
+ 'Analytics': stats.categories?.analytics?.total || 13,
630
+ 'Explorers': stats.categories?.block_explorers?.total || 6,
631
+ 'RPC': stats.categories?.rpc_nodes?.total || 8,
632
+ 'AI/ML': stats.categories?.ai_ml?.total || 1
633
+ }
634
+ };
635
+ } catch (error) {
636
+ console.error('[Dashboard] Resources fetch failed:', error);
637
+ return null;
638
+ }
639
+ }
640
+
641
+ async fetchNews() {
642
+ try {
643
+ // Try backend API first
644
+ let response = await apiClient.fetch('/api/news/latest?limit=6', {}, 10000);
645
+
646
+ if (response.ok) {
647
+ const data = await response.json();
648
+ this.newsCache = data.news || data.articles || [];
649
+ console.log('[Dashboard] News loaded from backend:', this.newsCache.length, 'articles');
650
+ return this.newsCache;
651
+ }
652
+
653
+ // Fallback to CryptoCompare direct
654
+ response = await fetch('https://min-api.cryptocompare.com/data/v2/news/?lang=EN');
655
+ if (response.ok) {
656
+ const data = await response.json();
657
+ if (data.Data) {
658
+ this.newsCache = data.Data.slice(0, 6).map(item => ({
659
+ id: item.id,
660
+ title: item.title,
661
+ summary: item.body?.substring(0, 150) + '...',
662
+ source: item.source,
663
+ published_at: new Date(item.published_on * 1000).toISOString(),
664
+ url: item.url
665
+ }));
666
+ console.log('[Dashboard] News loaded from CryptoCompare:', this.newsCache.length, 'articles');
667
+ return this.newsCache;
668
+ }
669
+ }
670
+
671
+ return [];
672
+ } catch (error) {
673
+ console.error('[Dashboard] News fetch failed:', error);
674
+ return [];
675
+ }
676
+ }
677
+
678
+ // ============================================================================
679
+ // FALLBACKS
680
+ // ============================================================================
681
+ // RENDER METHODS
682
+ // ============================================================================
683
+
684
+ /**
685
+ * Get coin image with fallback SVG
686
+ * @param {Object} coin - Coin data
687
+ * @returns {string} Image HTML with fallback
688
+ */
689
+ getCoinImage(coin, size = 32) {
690
+ const imageUrl = coin.image || `https://assets.coingecko.com/coins/images/1/small/${coin.id}.png`;
691
+ const symbol = (coin.symbol || '?').charAt(0).toUpperCase();
692
+ const fallbackSvg = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='${size}' height='${size}'%3E%3Ccircle cx='${size/2}' cy='${size/2}' r='${size/2-2}' fill='%2394a3b8'/%3E%3Ctext x='${size/2}' y='${size/2+size/4}' text-anchor='middle' fill='white' font-size='${size/2}' font-weight='bold'%3E${symbol}%3C/text%3E%3C/svg%3E`;
693
+
694
+ return `<img src="${imageUrl}"
695
+ alt="${coin.name || coin.symbol || 'Coin'}"
696
+ width="${size}"
697
+ height="${size}"
698
+ onerror="this.onerror=null; this.src='${fallbackSvg}';"
699
+ loading="lazy"
700
+ style="border-radius: 50%; object-fit: cover;">`;
701
+ }
702
+
703
+ renderStats(stats) {
704
+ const animate = (el, val, delay = 0) => {
705
+ if (!el) return;
706
+ setTimeout(() => {
707
+ el.classList.add('updating');
708
+ // Smooth count-up animation
709
+ const current = parseInt(el.textContent) || 0;
710
+ const target = val > 0 ? val : 0;
711
+ const duration = 800;
712
+ const steps = 30;
713
+ const increment = (target - current) / steps;
714
+ let step = 0;
715
+
716
+ const counter = setInterval(() => {
717
+ step++;
718
+ const newVal = Math.round(current + (increment * step));
719
+ el.textContent = formatNumber(newVal);
720
+
721
+ if (step >= steps) {
722
+ el.textContent = val > 0 ? formatNumber(val) : '--';
723
+ clearInterval(counter);
724
+ setTimeout(() => el.classList.remove('updating'), 300);
725
+ }
726
+ }, duration / steps);
727
+ }, delay);
728
+ };
729
+
730
+ // Stagger animations for smoother feel
731
+ animate(document.getElementById('stat-resources'), stats.total_resources, 0);
732
+ animate(document.getElementById('stat-apikeys'), stats.api_keys, 100);
733
+ animate(document.getElementById('stat-models'), stats.models_loaded, 200);
734
+ animate(document.getElementById('stat-providers'), stats.active_providers, 300);
735
+ }
736
+
737
+ renderTicker(data) {
738
+ const track = document.getElementById('ticker-track');
739
+ if (!track) return;
740
+
741
+ if (!data || !data.length) {
742
+ console.warn('[Dashboard] No ticker data available');
743
+ track.innerHTML = '<div style="padding: 8px 16px; color: var(--text-muted);">No market data available</div>';
744
+ return;
745
+ }
746
+
747
+ // ONE ROW TICKER - HORIZONTAL LAYOUT WITH REAL ICONS
748
+ const items = data.slice(0, 10).map(coin => {
749
+ const change = coin.price_change_percentage_24h || 0;
750
+ const cls = change >= 0 ? 'up' : 'down';
751
+ const arrow = change >= 0 ? 'β–²' : 'β–Ό';
752
+ const symbol = coin.symbol || coin.id || 'N/A';
753
+ const price = coin.current_price || 0;
754
+
755
+ // USE REAL CRYPTOCURRENCY ICONS FROM COINGECKO
756
+ const coinImage = coin.image || `https://assets.coingecko.com/coins/images/1/small/${coin.id}.png`;
757
+
758
+ return `
759
+ <div class="ticker-item">
760
+ <img src="${coinImage}" alt="${symbol}" width="20" height="20" style="border-radius: 50%;" onerror="this.style.display='none'">
761
+ <span class="ticker-symbol">${symbol.toUpperCase()}</span>
762
+ <span class="ticker-price">${formatCurrency(price)}</span>
763
+ <span class="ticker-change ${cls}">${arrow} ${Math.abs(change).toFixed(1)}%</span>
764
+ </div>
765
+ `;
766
+ }).join('');
767
+
768
+ track.innerHTML = items;
769
+ }
770
+
771
+ renderMarketTable(data) {
772
+ const container = document.getElementById('market-table-container');
773
+ if (!container) return;
774
+
775
+ if (!data || !data.length) {
776
+ container.innerHTML = '<div class="empty-state"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin: 0 auto 12px; opacity: 0.3;"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg><p>No market data available</p><p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">Please check your connection</p></div>';
777
+ return;
778
+ }
779
+
780
+ const rows = data.slice(0, 10).map((coin, i) => {
781
+ const change = coin.price_change_percentage_24h || 0;
782
+ const cls = change >= 0 ? 'up' : 'down';
783
+
784
+ // USE REAL CRYPTOCURRENCY ICONS FROM COINGECKO
785
+ const coinImage = coin.image || `https://assets.coingecko.com/coins/images/1/small/${coin.id}.png`;
786
+ const sparklineData = coin.sparkline_in_7d?.price || coin.sparkline?.price || this.generateSparkline(coin.current_price);
787
+
788
+ return `
789
+ <div class="market-row" data-id="${coin.id}">
790
+ <div class="market-rank">${coin.market_cap_rank || i + 1}</div>
791
+ <div class="market-coin">
792
+ <img src="${coinImage}" alt="${coin.name}" width="36" height="36" style="border-radius: 50%; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);" onerror="this.style.display='none'">
793
+ <div class="market-coin-info">
794
+ <span class="market-coin-name">${coin.name || 'Unknown'}</span>
795
+ <span class="market-coin-symbol" style="display: block; font-size: 11px; color: var(--text-muted); font-weight: 500; margin-top: 2px;">${(coin.symbol || coin.id || 'N/A').toUpperCase()}</span>
796
+ </div>
797
+ </div>
798
+ <div class="market-price">${formatCurrency(coin.current_price || 0)}</div>
799
+ <div class="market-change ${cls}">
800
+ <span class="change-badge ${cls}">
801
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
802
+ ${change >= 0 ? '<path d="m18 15-6-6-6 6"/>' : '<path d="m6 9 6 6 6-6"/>'}
803
+ </svg>
804
+ ${change >= 0 ? '+' : ''}${change.toFixed(2)}%
805
+ </span>
806
+ </div>
807
+ <div class="market-sparkline">${this.renderSparkline(sparklineData, change >= 0)}</div>
808
+ <div class="market-cap">${formatCurrency(coin.market_cap || 0)}</div>
809
+ <div class="market-actions">
810
+ <button class="btn-view" data-coin='${JSON.stringify(coin).replace(/'/g, "&apos;")}' title="View Details">
811
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
812
+ View
813
+ </button>
814
+ </div>
815
+ </div>
816
+ `;
817
+ }).join('');
818
+
819
+ container.innerHTML = `
820
+ <div class="market-header">
821
+ <span class="header-rank">#</span>
822
+ <span class="header-coin">COIN</span>
823
+ <span class="header-price">PRICE</span>
824
+ <span class="header-change">24H %</span>
825
+ <span class="header-chart">7D CHART</span>
826
+ <span class="header-mcap">MARKET CAP</span>
827
+ <span class="header-actions">ACTION</span>
828
+ </div>
829
+ <div class="market-body">${rows}</div>
830
+ `;
831
+
832
+ // Bind View buttons
833
+ container.querySelectorAll('.btn-view').forEach(btn => {
834
+ btn.addEventListener('click', () => {
835
+ try {
836
+ const coin = JSON.parse(btn.dataset.coin.replace(/&apos;/g, "'"));
837
+ this.showCoinDetailsModal(coin);
838
+ } catch (e) {
839
+ console.error('[Dashboard] Error parsing coin data:', e);
840
+ }
841
+ });
842
+ });
843
+ }
844
+
845
+ showCoinDetailsModal(coin) {
846
+ const change = coin.price_change_percentage_24h || 0;
847
+ const changeClass = change >= 0 ? 'positive' : 'negative';
848
+ const arrow = change >= 0 ? '↑' : '↓';
849
+
850
+ // USE REAL CRYPTOCURRENCY ICON
851
+ const coinImage = coin.image || `https://assets.coingecko.com/coins/images/1/small/${coin.id}.png`;
852
+
853
+ const modal = document.createElement('div');
854
+ modal.className = 'modal-overlay';
855
+ modal.innerHTML = `
856
+ <div class="modal-content coin-details-modal">
857
+ <div class="modal-header">
858
+ <div class="modal-title-group">
859
+ <img src="${coinImage}" alt="${coin.name}" width="48" height="48" style="border-radius: 50%; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);" onerror="this.style.display='none'">
860
+ <div>
861
+ <h2>${coin.name}</h2>
862
+ <p class="coin-symbol">${coin.symbol?.toUpperCase()}</p>
863
+ </div>
864
+ </div>
865
+ <button class="modal-close" onclick="this.closest('.modal-overlay').remove()">&times;</button>
866
+ </div>
867
+ <div class="modal-body">
868
+ <div class="coin-details-grid">
869
+ <div class="detail-card">
870
+ <span class="detail-label">Current Price</span>
871
+ <span class="detail-value">${formatCurrency(coin.current_price)}</span>
872
+ </div>
873
+ <div class="detail-card">
874
+ <span class="detail-label">24h Change</span>
875
+ <span class="detail-value ${changeClass}">${arrow} ${Math.abs(change).toFixed(2)}%</span>
876
+ </div>
877
+ <div class="detail-card">
878
+ <span class="detail-label">Market Cap</span>
879
+ <span class="detail-value">${formatCurrency(coin.market_cap)}</span>
880
+ </div>
881
+ <div class="detail-card">
882
+ <span class="detail-label">24h Volume</span>
883
+ <span class="detail-value">${formatCurrency(coin.total_volume)}</span>
884
+ </div>
885
+ <div class="detail-card">
886
+ <span class="detail-label">Market Cap Rank</span>
887
+ <span class="detail-value">#${coin.market_cap_rank || 'N/A'}</span>
888
+ </div>
889
+ <div class="detail-card">
890
+ <span class="detail-label">Circulating Supply</span>
891
+ <span class="detail-value">${coin.circulating_supply ? formatNumber(coin.circulating_supply) : 'N/A'}</span>
892
+ </div>
893
+ </div>
894
+ </div>
895
+ <div class="modal-footer">
896
+ <button class="btn-secondary" onclick="this.closest('.modal-overlay').remove()">Close</button>
897
+ <a href="/static/pages/market/index.html" class="btn-primary">View Full Market</a>
898
+ </div>
899
+ </div>
900
+ `;
901
+
902
+ document.body.appendChild(modal);
903
+
904
+ // Close on overlay click
905
+ modal.addEventListener('click', (e) => {
906
+ if (e.target === modal) {
907
+ modal.remove();
908
+ }
909
+ });
910
+ }
911
+
912
+ renderSparkline(data, isUp = true) {
913
+ if (!data || data.length < 2) {
914
+ // Generate a simple placeholder
915
+ const w = 80, h = 28;
916
+ const mid = h / 2;
917
+ const points = Array.from({length: 10}, (_, i) => `${(i / 9) * w},${mid + Math.sin(i) * 4}`).join(' ');
918
+ const color = '#94a3b8';
919
+ return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" style="opacity: 0.5;"><polyline fill="none" stroke="${color}" stroke-width="1.5" points="${points}"/></svg>`;
920
+ }
921
+ const w = 80, h = 28;
922
+ const min = Math.min(...data), max = Math.max(...data);
923
+ const range = max - min || 1;
924
+ const points = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - ((v - min) / range) * h}`).join(' ');
925
+ const color = isUp ? '#22c55e' : '#ef4444';
926
+ const fillColor = isUp ? 'rgba(34, 197, 94, 0.1)' : 'rgba(239, 68, 68, 0.1)';
927
+ return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
928
+ <defs>
929
+ <linearGradient id="grad-${isUp ? 'up' : 'down'}" x1="0%" y1="0%" x2="0%" y2="100%">
930
+ <stop offset="0%" style="stop-color:${fillColor};stop-opacity:1" />
931
+ <stop offset="100%" style="stop-color:${fillColor};stop-opacity:0" />
932
+ </linearGradient>
933
+ </defs>
934
+ <polygon fill="url(#grad-${isUp ? 'up' : 'down'})" points="${points} ${w},${h} 0,${h}"/>
935
+ <polyline fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="${points}"/>
936
+ </svg>`;
937
+ }
938
+
939
+ generateSparkline(base) {
940
+ const arr = [];
941
+ let p = base;
942
+ for (let i = 0; i < 24; i++) {
943
+ p *= 1 + (Math.random() - 0.5) * 0.02;
944
+ arr.push(p);
945
+ }
946
+ return arr;
947
+ }
948
+
949
+ renderSentimentChart(data, timeframe = '1D') {
950
+ if (!window.Chart) return;
951
+ const canvas = document.getElementById('sentiment-chart');
952
+ if (!canvas) return;
953
+
954
+ const value = data.fear_greed_index || 50;
955
+ const { labels, values } = this.generateSentimentData(value, timeframe);
956
+
957
+ // Render gauge
958
+ this.renderSentimentGauge(value);
959
+
960
+ if (this.charts.sentiment) {
961
+ this.charts.sentiment.data.labels = labels;
962
+ this.charts.sentiment.data.datasets[0].data = values;
963
+ this.charts.sentiment.update('active');
964
+ return;
965
+ }
966
+
967
+ const ctx = canvas.getContext('2d');
968
+ const gradient = ctx.createLinearGradient(0, 0, 0, 200);
969
+ gradient.addColorStop(0, 'rgba(45, 212, 191, 0.5)');
970
+ gradient.addColorStop(0.5, 'rgba(45, 212, 191, 0.2)');
971
+ gradient.addColorStop(1, 'rgba(45, 212, 191, 0)');
972
+
973
+ this.charts.sentiment = new Chart(ctx, {
974
+ type: 'line',
975
+ data: {
976
+ labels,
977
+ datasets: [{
978
+ data: values,
979
+ borderColor: '#2dd4bf',
980
+ backgroundColor: gradient,
981
+ borderWidth: 3,
982
+ tension: 0.4,
983
+ fill: true,
984
+ pointRadius: 0,
985
+ pointHoverRadius: 8,
986
+ pointHoverBackgroundColor: '#2dd4bf',
987
+ pointHoverBorderColor: '#ffffff',
988
+ pointHoverBorderWidth: 3
989
+ }]
990
+ },
991
+ options: {
992
+ responsive: true,
993
+ maintainAspectRatio: false,
994
+ animation: {
995
+ duration: 1500,
996
+ easing: 'easeInOutQuart'
997
+ },
998
+ plugins: {
999
+ legend: { display: false },
1000
+ tooltip: {
1001
+ backgroundColor: 'rgba(15, 23, 42, 0.95)',
1002
+ titleColor: '#ffffff',
1003
+ bodyColor: '#e2e8f0',
1004
+ borderColor: '#2dd4bf',
1005
+ borderWidth: 2,
1006
+ padding: 12,
1007
+ cornerRadius: 8,
1008
+ displayColors: false,
1009
+ callbacks: {
1010
+ label: (context) => `Fear & Greed: ${context.parsed.y.toFixed(0)}`
1011
+ }
1012
+ }
1013
+ },
1014
+ scales: {
1015
+ y: { min: 0, max: 100, display: false },
1016
+ x: { display: false }
1017
+ },
1018
+ interaction: { mode: 'index', intersect: false }
1019
+ }
1020
+ });
1021
+ }
1022
+
1023
+ renderSentimentGauge(value) {
1024
+ const gauge = document.getElementById('sentiment-gauge');
1025
+ if (!gauge) return;
1026
+
1027
+ let label = 'Neutral', color = '#eab308';
1028
+ if (value < 25) { label = 'Extreme Fear'; color = '#ef4444'; }
1029
+ else if (value < 45) { label = 'Fear'; color = '#f97316'; }
1030
+ else if (value < 55) { label = 'Neutral'; color = '#eab308'; }
1031
+ else if (value < 75) { label = 'Greed'; color = '#22c55e'; }
1032
+ else { label = 'Extreme Greed'; color = '#10b981'; }
1033
+
1034
+ gauge.innerHTML = `
1035
+ <div class="gauge-container">
1036
+ <div class="gauge-bar">
1037
+ <div class="gauge-fill" style="width: ${value}%; background: ${color};"></div>
1038
+ <div class="gauge-indicator" style="left: ${value}%;">
1039
+ <span class="gauge-value">${value}</span>
1040
+ </div>
1041
+ </div>
1042
+ <div class="gauge-labels">
1043
+ <span>Extreme Fear</span>
1044
+ <span>Neutral</span>
1045
+ <span>Extreme Greed</span>
1046
+ </div>
1047
+ <div class="gauge-result" style="color: ${color};">${label}</div>
1048
+ </div>
1049
+ `;
1050
+ }
1051
+
1052
+ async generateSentimentData(base, tf) {
1053
+ // Fetch real sentiment data from API
1054
+ try {
1055
+ const response = await fetch(`/api/sentiment/global?timeframe=${tf}`);
1056
+ if (response.ok) {
1057
+ const data = await response.json();
1058
+ if (data.history && data.history.length > 0) {
1059
+ const labels = data.history.map((item, i) => {
1060
+ if (i === data.history.length - 1) return 'Now';
1061
+ const diff = data.history.length - 1 - i;
1062
+ return `-${diff}${tf === '1D' ? 'h' : 'd'}`;
1063
+ });
1064
+ const values = data.history.map(item => item.sentiment || base);
1065
+ return { labels, values };
1066
+ }
1067
+ }
1068
+ } catch (error) {
1069
+ console.warn('Failed to fetch sentiment data, using fallback');
1070
+ }
1071
+
1072
+ // Fallback: return current sentiment only
1073
+ return {
1074
+ labels: ['Now'],
1075
+ values: [base]
1076
+ };
1077
+ }
1078
+
1079
+ updateSentimentTimeframe(tf) {
1080
+ this.fetchSentiment().then(data => this.renderSentimentChart(data, tf));
1081
+ }
1082
+
1083
+ renderResourcesChart(data) {
1084
+ if (!window.Chart) return;
1085
+ const canvas = document.getElementById('categories-chart');
1086
+ if (!canvas) return;
1087
+
1088
+ const categories = data.categories || {};
1089
+ const labels = Object.keys(categories);
1090
+ const values = Object.values(categories);
1091
+ const total = values.reduce((a, b) => a + b, 0);
1092
+
1093
+ // Update center - simple and clean
1094
+ const center = document.getElementById('donut-center');
1095
+ if (center) {
1096
+ const valueEl = center.querySelector('.donut-value');
1097
+ const labelEl = center.querySelector('.donut-label');
1098
+ valueEl.textContent = total;
1099
+ labelEl.textContent = 'RESOURCES';
1100
+ }
1101
+
1102
+ if (this.charts.categories) {
1103
+ this.charts.categories.data.labels = labels;
1104
+ this.charts.categories.data.datasets[0].data = values;
1105
+ this.charts.categories.update('none');
1106
+ return;
1107
+ }
1108
+
1109
+ // Clean, modern colors - solid, no gradients
1110
+ const colors = [
1111
+ '#8b5cf6', // Purple - Market
1112
+ '#2dd4bf', // Teal - News
1113
+ '#22c55e', // Green - Sentiment
1114
+ '#f97316', // Orange - Analytics
1115
+ '#ec4899', // Pink - Explorers
1116
+ '#3b82f6', // Blue - RPC
1117
+ '#fbbf24' // Yellow - AI/ML
1118
+ ];
1119
+
1120
+ const ctx = canvas.getContext('2d');
1121
+ this.charts.categories = new Chart(ctx, {
1122
+ type: 'doughnut',
1123
+ data: {
1124
+ labels,
1125
+ datasets: [{
1126
+ data: values,
1127
+ backgroundColor: colors,
1128
+ borderWidth: 8,
1129
+ borderColor: '#ffffff',
1130
+ hoverOffset: 8,
1131
+ hoverBorderWidth: 8
1132
+ }]
1133
+ },
1134
+ options: {
1135
+ responsive: true,
1136
+ maintainAspectRatio: false,
1137
+ cutout: '75%',
1138
+ animation: {
1139
+ animateRotate: true,
1140
+ duration: 800,
1141
+ easing: 'easeOutQuart'
1142
+ },
1143
+ plugins: {
1144
+ legend: {
1145
+ display: false
1146
+ },
1147
+ tooltip: {
1148
+ enabled: false
1149
+ }
1150
+ },
1151
+ interaction: {
1152
+ mode: 'nearest',
1153
+ intersect: true
1154
+ }
1155
+ }
1156
+ });
1157
+ }
1158
+
1159
+ // Watchlist removed - not needed in dashboard
1160
+
1161
+ renderNewsAccordion(news) {
1162
+ const container = document.getElementById('news-accordion');
1163
+ if (!container) return;
1164
+
1165
+ // ONLY SHOW REAL NEWS - NO DEMO DATA
1166
+ if (!news || !news.length) {
1167
+ container.innerHTML = `
1168
+ <div class="empty-state small" style="padding: 20px; text-align: center;">
1169
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin: 0 auto 12px; opacity: 0.3;">
1170
+ <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
1171
+ </svg>
1172
+ <p style="color: var(--text-muted); font-size: 13px;">No news available</p>
1173
+ <p style="color: var(--text-light); font-size: 11px; margin-top: 4px;">News API is not responding</p>
1174
+ </div>
1175
+ `;
1176
+ return;
1177
+ }
1178
+
1179
+ const items = news.slice(0, this.config.maxNewsItems).map((item, i) => {
1180
+ const isExpanded = this.expandedNews.has(i);
1181
+ const time = this.formatRelativeTime(item.published_at);
1182
+ return `
1183
+ <div class="accordion-item ${isExpanded ? 'expanded' : ''}" data-index="${i}">
1184
+ <div class="accordion-header">
1185
+ <div class="accordion-title">
1186
+ <span class="news-source-badge">${item.source || 'News'}</span>
1187
+ <span class="news-title-text">${item.title}</span>
1188
+ </div>
1189
+ <div class="accordion-meta">
1190
+ <span class="news-time">${time}</span>
1191
+ <svg class="accordion-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
1192
+ </div>
1193
+ </div>
1194
+ <div class="accordion-body">
1195
+ <p class="news-summary">${item.summary || item.description || 'No summary available.'}</p>
1196
+ <a href="${item.url || '#'}" class="news-link" target="_blank" rel="noopener">Read full article β†’</a>
1197
+ </div>
1198
+ </div>
1199
+ `;
1200
+ }).join('');
1201
+
1202
+ container.innerHTML = items;
1203
+
1204
+ // Bind accordion toggle
1205
+ container.querySelectorAll('.accordion-header').forEach(header => {
1206
+ header.addEventListener('click', () => {
1207
+ const item = header.closest('.accordion-item');
1208
+ const index = parseInt(item.dataset.index);
1209
+ item.classList.toggle('expanded');
1210
+ if (this.expandedNews.has(index)) {
1211
+ this.expandedNews.delete(index);
1212
+ } else {
1213
+ this.expandedNews.add(index);
1214
+ }
1215
+ });
1216
+ });
1217
+ }
1218
+
1219
+ renderAlerts() {
1220
+ const container = document.getElementById('alerts-list');
1221
+ if (!container) return;
1222
+
1223
+ if (!this.priceAlerts.length) {
1224
+ container.innerHTML = '<div class="empty-state small">No alerts set</div>';
1225
+ return;
1226
+ }
1227
+
1228
+ container.innerHTML = this.priceAlerts.map((alert, i) => `
1229
+ <div class="alert-item ${alert.triggered ? 'triggered' : ''}">
1230
+ <div class="alert-icon">${alert.type === 'above' ? 'πŸ“ˆ' : 'πŸ“‰'}</div>
1231
+ <div class="alert-info">
1232
+ <span class="alert-symbol">${alert.symbol}</span>
1233
+ <span class="alert-condition">${alert.type === 'above' ? '>' : '<'} ${formatCurrency(alert.price)}</span>
1234
+ </div>
1235
+ <button class="remove-btn" data-index="${i}">Γ—</button>
1236
+ </div>
1237
+ `).join('');
1238
+
1239
+ container.querySelectorAll('.remove-btn').forEach(btn => {
1240
+ btn.addEventListener('click', () => {
1241
+ this.priceAlerts.splice(parseInt(btn.dataset.index), 1);
1242
+ this.savePersistedData();
1243
+ this.renderAlerts();
1244
+ });
1245
+ });
1246
+ }
1247
+
1248
+ async renderMiniStats() {
1249
+ // Fetch real system stats from API
1250
+ try {
1251
+ const response = await fetch('/api/status');
1252
+ if (response.ok) {
1253
+ const data = await response.json();
1254
+
1255
+ const el1 = document.getElementById('stat-response');
1256
+ const el2 = document.getElementById('stat-cache');
1257
+ const el3 = document.getElementById('stat-sessions');
1258
+
1259
+ if (el1) el1.textContent = `${data.avg_response_time || 0}ms`;
1260
+ if (el2) el2.textContent = `${data.cache_hit_rate || 0}%`;
1261
+ if (el3) el3.textContent = data.active_connections || 0;
1262
+ return;
1263
+ }
1264
+ } catch (error) {
1265
+ console.warn('Failed to fetch system stats');
1266
+ }
1267
+
1268
+ // Fallback: show N/A
1269
+ const el1 = document.getElementById('stat-response');
1270
+ const el2 = document.getElementById('stat-cache');
1271
+ const el3 = document.getElementById('stat-sessions');
1272
+
1273
+ if (el1) el1.textContent = 'N/A';
1274
+ if (el2) el2.textContent = 'N/A';
1275
+ if (el3) el3.textContent = 'N/A';
1276
+ }
1277
+
1278
+ // ============================================================================
1279
+ // HELPERS
1280
+ // ============================================================================
1281
+
1282
+ // Watchlist methods removed - not needed in dashboard
1283
+
1284
+ showAddAlertModal() {
1285
+ const symbol = prompt('Enter symbol (e.g., BTC):');
1286
+ if (!symbol) return;
1287
+ const price = parseFloat(prompt('Target price:'));
1288
+ if (isNaN(price)) return;
1289
+ const type = confirm('Alert when ABOVE? (Cancel for below)') ? 'above' : 'below';
1290
+ this.priceAlerts.push({ symbol: symbol.toUpperCase(), price, type, triggered: false });
1291
+ this.savePersistedData();
1292
+ this.renderAlerts();
1293
+ this.showToast('Alert created', 'success');
1294
+ }
1295
+
1296
+ filterMarketTable(q) {
1297
+ if (!this.marketData) return;
1298
+ const filtered = q ? this.marketData.filter(c => c.name?.toLowerCase().includes(q.toLowerCase()) || c.symbol?.toLowerCase().includes(q.toLowerCase())) : this.marketData;
1299
+ this.renderMarketTable(filtered);
1300
+ }
1301
+
1302
+ sortMarketData(by) {
1303
+ if (!this.marketData) return;
1304
+ const sorted = [...this.marketData].sort((a, b) => {
1305
+ if (by === 'price') return (b.current_price || 0) - (a.current_price || 0);
1306
+ if (by === 'change') return Math.abs(b.price_change_percentage_24h || 0) - Math.abs(a.price_change_percentage_24h || 0);
1307
+ return (a.market_cap_rank || 0) - (b.market_cap_rank || 0);
1308
+ });
1309
+ this.renderMarketTable(sorted);
1310
+ }
1311
+
1312
+ formatRelativeTime(date) {
1313
+ if (!date) return '';
1314
+ const diff = Date.now() - new Date(date).getTime();
1315
+ const min = Math.floor(diff / 60000);
1316
+ if (min < 60) return `${min}m ago`;
1317
+ const hr = Math.floor(min / 60);
1318
+ if (hr < 24) return `${hr}h ago`;
1319
+ return `${Math.floor(hr / 24)}d ago`;
1320
+ }
1321
+
1322
+ updateTimestamp() {
1323
+ const el = document.getElementById('last-update');
1324
+ if (el) el.textContent = new Date().toLocaleTimeString();
1325
+ }
1326
+
1327
+ showToast(msg, type = 'info') {
1328
+ const colors = { success: '#22c55e', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
1329
+ const toast = document.createElement('div');
1330
+ toast.className = 'toast-notification';
1331
+ toast.style.cssText = `position:fixed;top:20px;right:20px;padding:12px 20px;border-radius:12px;background:${colors[type]};color:#fff;z-index:9999;animation:slideIn .3s ease;font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.3);`;
1332
+ toast.textContent = msg;
1333
+ document.body.appendChild(toast);
1334
+ setTimeout(() => { toast.style.animation = 'slideOut .3s ease'; setTimeout(() => toast.remove(), 300); }, 3000);
1335
+ }
1336
+ }
1337
+
1338
+ // Initialize
1339
+ const dashboard = new DashboardPage();
1340
+ window.dashboardPage = dashboard;
1341
+ if (document.readyState === 'loading') {
1342
+ document.addEventListener('DOMContentLoaded', () => dashboard.init());
1343
+ } else {
1344
+ setTimeout(() => dashboard.init(), 0);
1345
+ }
1346
+
1347
+ export default dashboard;