Really-amin commited on
Commit
dd7ffbd
·
verified ·
1 Parent(s): 322f1ff

Upload 196 files

Browse files
.dockerignore CHANGED
@@ -1,16 +1,121 @@
1
- __pycache__/
2
- *.pyc
3
- *.pyo
4
- *.log
5
- .venv/
6
- venv/
7
- .git/
8
- .gitignore
9
- dist/
10
- build/
11
- *.zip
12
- *.tar*
13
- *.db
14
- *.sqlite
15
- *.sqlite3
16
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+ pip-log.txt
24
+ pip-delete-this-directory.txt
25
+
26
+ # Virtual environments
27
+ venv/
28
+ ENV/
29
+ env/
30
+ .venv
31
+
32
+ # IDE
33
+ .vscode/
34
+ .idea/
35
+ *.swp
36
+ *.swo
37
+ *~
38
+ .DS_Store
39
+
40
+ # Git
41
+ .git/
42
+ .gitignore
43
+ .gitattributes
44
+
45
+ # Documentation
46
+ *.md
47
+ docs/
48
+ README*.md
49
+ CHANGELOG.md
50
+ LICENSE
51
+
52
+ # Testing
53
+ .pytest_cache/
54
+ .coverage
55
+ htmlcov/
56
+ .tox/
57
+ .hypothesis/
58
+ tests/
59
+ test_*.py
60
+
61
+ # Logs and databases (will be created in container)
62
+ *.log
63
+ logs/
64
+ data/*.db
65
+ data/*.sqlite
66
+ data/*.db-journal
67
+
68
+ # Environment files (should be set via docker-compose or HF Secrets)
69
+ .env
70
+ .env.*
71
+ !.env.example
72
+
73
+ # Docker
74
+ docker-compose*.yml
75
+ !docker-compose.yml
76
+ Dockerfile
77
+ .dockerignore
78
+
79
+ # CI/CD
80
+ .github/
81
+ .gitlab-ci.yml
82
+ .travis.yml
83
+ azure-pipelines.yml
84
+
85
+ # Temporary files
86
+ *.tmp
87
+ *.bak
88
+ *.swp
89
+ temp/
90
+ tmp/
91
+
92
+ # Node modules (if any)
93
+ node_modules/
94
+ package-lock.json
95
+ yarn.lock
96
+
97
+ # OS files
98
+ Thumbs.db
99
+ .DS_Store
100
+ desktop.ini
101
+
102
+ # Jupyter notebooks
103
+ .ipynb_checkpoints/
104
+ *.ipynb
105
+
106
+ # Model cache (models will be downloaded in container)
107
+ models/
108
+ .cache/
109
+ .huggingface/
110
+
111
+ # Large files that shouldn't be in image
112
+ *.tar
113
+ *.tar.gz
114
+ *.zip
115
+ *.rar
116
+ *.7z
117
+
118
+ # Screenshots and assets not needed
119
+ screenshots/
120
+ assets/*.png
121
+ assets/*.jpg
UI_DEMO.html ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>UI Components Demo - Crypto Intelligence Hub</title>
7
+
8
+ <!-- Fonts -->
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
11
+
12
+ <!-- Icons -->
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
14
+
15
+ <!-- Styles -->
16
+ <link rel="stylesheet" href="/static/css/main.css">
17
+ <link rel="stylesheet" href="/static/css/toast.css">
18
+ <link rel="stylesheet" href="/static/css/enhancements.css">
19
+
20
+ <style>
21
+ .demo-section {
22
+ margin-bottom: 40px;
23
+ padding: 30px;
24
+ background: rgba(17, 24, 39, 0.6);
25
+ border: 1px solid var(--border);
26
+ border-radius: 16px;
27
+ }
28
+
29
+ .demo-title {
30
+ font-size: 24px;
31
+ font-weight: 700;
32
+ margin-bottom: 20px;
33
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
34
+ -webkit-background-clip: text;
35
+ -webkit-text-fill-color: transparent;
36
+ background-clip: text;
37
+ }
38
+
39
+ .demo-grid {
40
+ display: grid;
41
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
42
+ gap: 15px;
43
+ margin-top: 20px;
44
+ }
45
+
46
+ .code-block {
47
+ background: rgba(0, 0, 0, 0.4);
48
+ border: 1px solid var(--border);
49
+ border-radius: 8px;
50
+ padding: 15px;
51
+ font-family: 'JetBrains Mono', monospace;
52
+ font-size: 13px;
53
+ color: #a5d6ff;
54
+ margin-top: 10px;
55
+ overflow-x: auto;
56
+ }
57
+ </style>
58
+ </head>
59
+ <body>
60
+ <div class="toast-container" id="toast-container"></div>
61
+
62
+ <div class="app-container">
63
+ <header class="app-header">
64
+ <div class="header-content">
65
+ <div class="logo">
66
+ <div class="logo-icon">
67
+ <i class="fas fa-palette"></i>
68
+ </div>
69
+ <div class="logo-text">
70
+ <h1>UI Components Demo</h1>
71
+ <p><i class="fas fa-code"></i> Crypto Intelligence Hub Design System</p>
72
+ </div>
73
+ </div>
74
+ <button class="theme-toggle" onclick="toggleTheme()">
75
+ <i class="fas fa-moon"></i>
76
+ </button>
77
+ </div>
78
+ </header>
79
+
80
+ <main class="main-content">
81
+ <!-- Toast Notifications -->
82
+ <section class="demo-section">
83
+ <h2 class="demo-title">🔔 Toast Notifications</h2>
84
+ <p style="color: var(--text-secondary); margin-bottom: 20px;">
85
+ Modern slide-in notifications with auto-dismiss
86
+ </p>
87
+
88
+ <div class="demo-grid">
89
+ <button class="btn-primary" onclick="showToast('success')">
90
+ <i class="fas fa-check"></i> Success Toast
91
+ </button>
92
+ <button class="btn-primary" onclick="showToast('error')">
93
+ <i class="fas fa-times"></i> Error Toast
94
+ </button>
95
+ <button class="btn-primary" onclick="showToast('warning')">
96
+ <i class="fas fa-exclamation"></i> Warning Toast
97
+ </button>
98
+ <button class="btn-primary" onclick="showToast('info')">
99
+ <i class="fas fa-info"></i> Info Toast
100
+ </button>
101
+ </div>
102
+
103
+ <div class="code-block">ToastManager.success('Operation completed!');</div>
104
+ </section>
105
+
106
+ <!-- Stat Cards -->
107
+ <section class="demo-section">
108
+ <h2 class="demo-title">📊 Stat Cards</h2>
109
+ <div class="stats-grid">
110
+ <div class="stat-card gradient-purple">
111
+ <div class="stat-icon"><i class="fas fa-database"></i></div>
112
+ <div class="stat-content">
113
+ <div class="stat-value">1,234</div>
114
+ <div class="stat-label">Total Resources</div>
115
+ <div class="stat-trend"><i class="fas fa-arrow-up"></i> +12% this month</div>
116
+ </div>
117
+ </div>
118
+ <div class="stat-card gradient-green">
119
+ <div class="stat-icon"><i class="fas fa-gift"></i></div>
120
+ <div class="stat-content">
121
+ <div class="stat-value">567</div>
122
+ <div class="stat-label">Free Resources</div>
123
+ <div class="stat-trend"><i class="fas fa-check-circle"></i> Available</div>
124
+ </div>
125
+ </div>
126
+ <div class="stat-card gradient-blue">
127
+ <div class="stat-icon"><i class="fas fa-robot"></i></div>
128
+ <div class="stat-content">
129
+ <div class="stat-value">89</div>
130
+ <div class="stat-label">AI Models</div>
131
+ <div class="stat-trend"><i class="fas fa-brain"></i> Ready</div>
132
+ </div>
133
+ </div>
134
+ <div class="stat-card gradient-orange">
135
+ <div class="stat-icon"><i class="fas fa-server"></i></div>
136
+ <div class="stat-content">
137
+ <div class="stat-value">45</div>
138
+ <div class="stat-label">Providers</div>
139
+ <div class="stat-trend"><i class="fas fa-signal"></i> Online</div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </section>
144
+
145
+ <!-- Sentiment Badges -->
146
+ <section class="demo-section">
147
+ <h2 class="demo-title">💭 Sentiment Badges</h2>
148
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
149
+ <span class="sentiment-badge bullish">
150
+ <i class="fas fa-arrow-up"></i> Bullish
151
+ </span>
152
+ <span class="sentiment-badge bearish">
153
+ <i class="fas fa-arrow-down"></i> Bearish
154
+ </span>
155
+ <span class="sentiment-badge neutral">
156
+ <i class="fas fa-minus"></i> Neutral
157
+ </span>
158
+ </div>
159
+
160
+ <div class="code-block">&lt;span class="sentiment-badge bullish"&gt;Bullish&lt;/span&gt;</div>
161
+ </section>
162
+
163
+ <!-- Model Status -->
164
+ <section class="demo-section">
165
+ <h2 class="demo-title">🤖 Model Status Indicators</h2>
166
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
167
+ <span class="model-status available">
168
+ <i class="fas fa-check"></i> Available
169
+ </span>
170
+ <span class="model-status unavailable">
171
+ <i class="fas fa-times"></i> Unavailable
172
+ </span>
173
+ <span class="model-status partial">
174
+ <i class="fas fa-exclamation"></i> Partial
175
+ </span>
176
+ </div>
177
+ </section>
178
+
179
+ <!-- Loading States -->
180
+ <section class="demo-section">
181
+ <h2 class="demo-title">⏳ Loading States</h2>
182
+ <div class="demo-grid">
183
+ <div class="card">
184
+ <h3>Spinner</h3>
185
+ <div class="loading">
186
+ <div class="spinner"></div>
187
+ <p>Loading data...</p>
188
+ </div>
189
+ </div>
190
+ <div class="card">
191
+ <h3>Skeleton</h3>
192
+ <div class="skeleton skeleton-title"></div>
193
+ <div class="skeleton skeleton-text"></div>
194
+ <div class="skeleton skeleton-text"></div>
195
+ </div>
196
+ </div>
197
+ </section>
198
+
199
+ <!-- Glassmorphism -->
200
+ <section class="demo-section">
201
+ <h2 class="demo-title">✨ Glassmorphism Effects</h2>
202
+ <div class="glass-card" style="padding: 30px; text-align: center;">
203
+ <h3 class="neon-text">Frosted Glass Card</h3>
204
+ <p style="color: var(--text-secondary);">
205
+ Semi-transparent background with backdrop blur
206
+ </p>
207
+ </div>
208
+
209
+ <div class="code-block">&lt;div class="glass-card"&gt;...&lt;/div&gt;</div>
210
+ </section>
211
+
212
+ <!-- Market Table -->
213
+ <section class="demo-section">
214
+ <h2 class="demo-title">📈 Market Table</h2>
215
+ <table class="market-table">
216
+ <thead>
217
+ <tr>
218
+ <th>Coin</th>
219
+ <th>Price</th>
220
+ <th>24h Change</th>
221
+ <th>Market Cap</th>
222
+ </tr>
223
+ </thead>
224
+ <tbody>
225
+ <tr>
226
+ <td class="coin-info">
227
+ <div style="width: 32px; height: 32px; background: linear-gradient(135deg, #f7931a, #f7931a); border-radius: 50%;"></div>
228
+ <div>
229
+ <div class="coin-symbol">BTC</div>
230
+ <div class="coin-name">Bitcoin</div>
231
+ </div>
232
+ </td>
233
+ <td class="price">$45,678.90</td>
234
+ <td class="change-positive">+5.67%</td>
235
+ <td>$890.5B</td>
236
+ </tr>
237
+ <tr>
238
+ <td class="coin-info">
239
+ <div style="width: 32px; height: 32px; background: linear-gradient(135deg, #627eea, #627eea); border-radius: 50%;"></div>
240
+ <div>
241
+ <div class="coin-symbol">ETH</div>
242
+ <div class="coin-name">Ethereum</div>
243
+ </div>
244
+ </td>
245
+ <td class="price">$2,345.67</td>
246
+ <td class="change-negative">-2.34%</td>
247
+ <td>$282.1B</td>
248
+ </tr>
249
+ </tbody>
250
+ </table>
251
+ </section>
252
+
253
+ <!-- Buttons -->
254
+ <section class="demo-section">
255
+ <h2 class="demo-title">🔘 Buttons</h2>
256
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
257
+ <button class="btn-primary">
258
+ <i class="fas fa-rocket"></i> Primary Button
259
+ </button>
260
+ <button class="btn-refresh">
261
+ <i class="fas fa-sync"></i> Refresh Button
262
+ </button>
263
+ <button class="btn-primary" disabled>
264
+ <i class="fas fa-ban"></i> Disabled
265
+ </button>
266
+ </div>
267
+ </section>
268
+
269
+ <!-- Empty State -->
270
+ <section class="demo-section">
271
+ <h2 class="demo-title">📭 Empty State</h2>
272
+ <div class="empty-state">
273
+ <div class="empty-state-icon">
274
+ <i class="fas fa-inbox"></i>
275
+ </div>
276
+ <div class="empty-state-title">No Data Available</div>
277
+ <div class="empty-state-message">
278
+ There's nothing to display right now. Try refreshing or check back later.
279
+ </div>
280
+ <button class="btn-primary" style="margin-top: 20px;">
281
+ <i class="fas fa-sync"></i> Refresh
282
+ </button>
283
+ </div>
284
+ </section>
285
+
286
+ <!-- Alerts -->
287
+ <section class="demo-section">
288
+ <h2 class="demo-title">⚠️ Alerts</h2>
289
+ <div class="alert alert-success">
290
+ <strong>Success!</strong> Your operation completed successfully.
291
+ </div>
292
+ <div class="alert alert-error">
293
+ <strong>Error!</strong> Something went wrong. Please try again.
294
+ </div>
295
+ <div class="alert alert-warning">
296
+ <strong>Warning!</strong> You're approaching your rate limit.
297
+ </div>
298
+ </section>
299
+ </main>
300
+
301
+ <footer class="app-footer">
302
+ <p>Crypto Intelligence Hub - Modern UI Components</p>
303
+ <p>Built with Vanilla JavaScript & CSS</p>
304
+ </footer>
305
+ </div>
306
+
307
+ <script>
308
+ // Toast Manager
309
+ const ToastManager = {
310
+ container: null,
311
+
312
+ init() {
313
+ this.container = document.getElementById('toast-container');
314
+ },
315
+
316
+ show(message, type = 'info', duration = 5000) {
317
+ if (!this.container) this.init();
318
+
319
+ const toast = document.createElement('div');
320
+ toast.className = `toast ${type}`;
321
+
322
+ const icons = {
323
+ success: 'fa-check-circle',
324
+ error: 'fa-times-circle',
325
+ warning: 'fa-exclamation-triangle',
326
+ info: 'fa-info-circle'
327
+ };
328
+
329
+ const titles = {
330
+ success: 'Success',
331
+ error: 'Error',
332
+ warning: 'Warning',
333
+ info: 'Info'
334
+ };
335
+
336
+ toast.innerHTML = `
337
+ <div class="toast-icon">
338
+ <i class="fas ${icons[type]}"></i>
339
+ </div>
340
+ <div class="toast-content">
341
+ <div class="toast-title">${titles[type]}</div>
342
+ <div class="toast-message">${message}</div>
343
+ </div>
344
+ <button class="toast-close" onclick="ToastManager.remove(this.parentElement)">
345
+ <i class="fas fa-times"></i>
346
+ </button>
347
+ <div class="toast-progress"></div>
348
+ `;
349
+
350
+ this.container.appendChild(toast);
351
+
352
+ if (duration > 0) {
353
+ setTimeout(() => this.remove(toast), duration);
354
+ }
355
+ },
356
+
357
+ remove(toast) {
358
+ if (!toast) return;
359
+ toast.classList.add('removing');
360
+ setTimeout(() => {
361
+ if (toast.parentElement) {
362
+ toast.parentElement.removeChild(toast);
363
+ }
364
+ }, 300);
365
+ },
366
+
367
+ success(message) { this.show(message, 'success'); },
368
+ error(message) { this.show(message, 'error'); },
369
+ warning(message) { this.show(message, 'warning'); },
370
+ info(message) { this.show(message, 'info'); }
371
+ };
372
+
373
+ ToastManager.init();
374
+
375
+ function showToast(type) {
376
+ const messages = {
377
+ success: 'Operation completed successfully!',
378
+ error: 'An error occurred. Please try again.',
379
+ warning: 'Warning: Please review your input.',
380
+ info: 'Here\'s some useful information.'
381
+ };
382
+ ToastManager[type](messages[type]);
383
+ }
384
+
385
+ function toggleTheme() {
386
+ const body = document.body;
387
+ const icon = document.querySelector('.theme-toggle i');
388
+
389
+ if (body.classList.contains('light-theme')) {
390
+ body.classList.remove('light-theme');
391
+ icon.className = 'fas fa-moon';
392
+ } else {
393
+ body.classList.add('light-theme');
394
+ icon.className = 'fas fa-sun';
395
+ }
396
+ }
397
+ </script>
398
+ </body>
399
+ </html>
400
+
ai_models.py CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
5
  import logging
6
  import os
7
  import threading
 
8
  from dataclasses import dataclass
9
  from typing import Any, Dict, List, Mapping, Optional, Sequence
10
  from config import HUGGINGFACE_MODELS, get_settings
@@ -41,8 +42,19 @@ if HF_MODE not in ("off", "public", "auth"):
41
  logger.warning(f"Invalid HF_MODE, resetting to 'off'")
42
 
43
  if HF_MODE == "auth" and not HF_TOKEN_ENV:
44
- HF_MODE = "off"
45
- logger.warning("HF_MODE='auth' but no HF_TOKEN found, resetting to 'off'")
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  # Linked models in HF Space - these are pre-validated
48
  LINKED_MODEL_IDS = {
@@ -52,6 +64,8 @@ LINKED_MODEL_IDS = {
52
  "ElKulako/cryptobert",
53
  "kk08/CryptoBERT",
54
  "agarkovv/CryptoTrader-LM",
 
 
55
  "burakutf/finetuned-finbert-crypto",
56
  "mathugo/crypto_news_bert",
57
  "mayurjadhav/crypto-sentiment-model",
@@ -60,18 +74,32 @@ LINKED_MODEL_IDS = {
60
  # Extended Model Catalog - Using VERIFIED public models only
61
  # These models are tested and confirmed working on HuggingFace Hub
62
  CRYPTO_SENTIMENT_MODELS = [
63
- "cardiffnlp/twitter-roberta-base-sentiment-latest", # Verified working
 
 
64
  ]
65
  SOCIAL_SENTIMENT_MODELS = [
66
- "cardiffnlp/twitter-roberta-base-sentiment-latest", # Verified working
 
67
  ]
68
  FINANCIAL_SENTIMENT_MODELS = [
69
- "cardiffnlp/twitter-roberta-base-sentiment-latest", # Verified working
 
 
70
  ]
71
  NEWS_SENTIMENT_MODELS = [
72
- "cardiffnlp/twitter-roberta-base-sentiment-latest", # Verified working
 
 
 
 
 
 
 
 
 
 
73
  ]
74
- DECISION_MODELS = [] # Disable for now
75
 
76
  @dataclass(frozen=True)
77
  class PipelineSpec:
@@ -93,39 +121,255 @@ for lk in ["sentiment_twitter", "sentiment_financial", "summarization", "crypto_
93
  category="legacy"
94
  )
95
 
96
- # Crypto sentiment
97
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
98
- MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
99
- key=f"crypto_sent_{i}", task="text-classification", model_id=mid,
100
- category="crypto_sentiment", requires_auth=("ElKulako" in mid)
 
101
  )
102
 
 
 
 
 
 
 
103
  # Social
104
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
105
- MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
106
- key=f"social_sent_{i}", task="text-classification", model_id=mid, category="social_sentiment"
 
 
107
  )
108
 
 
 
 
 
 
 
109
  # Financial
110
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
111
- MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
112
- key=f"financial_sent_{i}", task="text-classification", model_id=mid, category="financial_sentiment"
 
113
  )
114
 
 
 
 
 
 
 
115
  # News
116
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
117
- MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
118
- key=f"news_sent_{i}", task="text-classification", model_id=mid, category="news_sentiment"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  )
120
 
121
  class ModelNotAvailable(RuntimeError): pass
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  class ModelRegistry:
124
  def __init__(self):
125
  self._pipelines = {}
126
  self._lock = threading.Lock()
127
  self._initialized = False
128
  self._failed_models = {} # Track failed models with reasons
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  def _should_use_token(self, spec: PipelineSpec) -> Optional[str]:
131
  """Determine if and which token to use for model loading"""
@@ -148,18 +392,25 @@ class ModelRegistry:
148
  return None
149
 
150
  def get_pipeline(self, key: str):
151
- """Get pipeline for a model key, with robust error handling"""
152
  if HF_MODE == "off":
153
- raise ModelNotAvailable("HF_MODE=off")
154
  if not TRANSFORMERS_AVAILABLE:
155
- raise ModelNotAvailable("transformers not installed")
156
  if key not in MODEL_SPECS:
157
- raise ModelNotAvailable(f"Unknown key: {key}")
158
 
159
  spec = MODEL_SPECS[key]
160
 
161
- # Return cached pipeline if available
 
 
 
 
 
 
162
  if key in self._pipelines:
 
163
  return self._pipelines[key]
164
 
165
  # Check if this model already failed
@@ -176,7 +427,7 @@ class ModelRegistry:
176
  # Determine token usage
177
  auth_token = self._should_use_token(spec)
178
 
179
- logger.info(f"Loading model: {spec.model_id} (mode={HF_MODE}, auth={'yes' if auth_token else 'no'})")
180
 
181
  try:
182
  # Use token parameter instead of deprecated use_auth_token
@@ -188,12 +439,18 @@ class ModelRegistry:
188
  # Only add token if we have one and it's needed
189
  if auth_token:
190
  pipeline_kwargs["token"] = auth_token
 
191
  else:
192
  # Explicitly set to None to avoid using expired tokens
193
  pipeline_kwargs["token"] = None
 
 
194
 
 
195
  self._pipelines[key] = pipeline(**pipeline_kwargs)
196
- logger.info(f"Successfully loaded model: {spec.model_id}")
 
 
197
  return self._pipelines[key]
198
 
199
  except RepositoryNotFoundError as e:
@@ -210,7 +467,8 @@ class ModelRegistry:
210
  if REQUESTS_AVAILABLE and isinstance(e, requests.exceptions.HTTPError):
211
  status_code = getattr(e.response, 'status_code', None)
212
  if status_code == 401:
213
- error_msg = f"Authentication failed (401) for {spec.model_id}"
 
214
  elif status_code == 403:
215
  error_msg = f"Access forbidden (403) for {spec.model_id}"
216
  elif status_code == 404:
@@ -221,22 +479,86 @@ class ModelRegistry:
221
  if "not a valid model identifier" in str(e):
222
  # For linked models in HF Space, skip validation error
223
  if spec.model_id in LINKED_MODEL_IDS:
224
- logger.info(f"Linked model {spec.model_id} - trying without validation check")
225
  # Don't mark as failed yet, it might work
226
  pass
227
  else:
228
  error_msg = f"Invalid model identifier: {spec.model_id}"
229
  elif "401" in str(e) or "403" in str(e):
230
- error_msg = f"Authentication required for {spec.model_id}"
 
231
  else:
232
  error_msg = f"OS Error loading {spec.model_id}: {str(e)[:100]}"
233
 
234
- logger.warning(f"Failed to load {spec.model_id}: {error_msg}")
235
  self._failed_models[key] = error_msg
 
 
236
  raise ModelNotAvailable(error_msg) from e
237
 
238
  return self._pipelines[key]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  def initialize_models(self):
241
  """Initialize models with fallback logic - tries primary models first"""
242
  if self._initialized:
@@ -267,37 +589,59 @@ class ModelRegistry:
267
  "error": "transformers library not installed - using lexical fallback"
268
  }
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  loaded, failed = [], []
271
 
272
  # Try to load at least one model from each category with fallback
273
  categories_to_try = {
274
- "crypto": ["crypto_sent_0"],
275
- "financial": ["financial_sent_0"],
276
- "social": ["social_sent_0"],
277
  "news": ["news_sent_0"]
278
  }
279
 
 
 
280
  for category, keys in categories_to_try.items():
281
  category_loaded = False
282
  for key in keys:
283
  if key not in MODEL_SPECS:
284
  continue
285
  try:
 
286
  self.get_pipeline(key)
287
  loaded.append(key)
288
  category_loaded = True
 
289
  break # Successfully loaded one from this category
290
  except ModelNotAvailable as e:
291
- failed.append((key, str(e)[:100])) # Truncate long errors
 
 
292
  except Exception as e:
293
- failed.append((key, f"{type(e).__name__}: {str(e)[:100]}"))
 
 
294
 
295
  # Determine status - be more lenient
296
  if len(loaded) > 0:
297
  status = "ok"
 
298
  else:
299
  # No models loaded, but that's OK - we have fallback
300
- logger.warning("No HF models loaded, using fallback-only mode")
301
  status = "fallback_only"
302
 
303
  self._initialized = True
@@ -310,15 +654,28 @@ class ModelRegistry:
310
  "loaded": loaded[:10], # Limit to first 10 for brevity
311
  "failed": failed[:10], # Limit to first 10 for brevity
312
  "failed_count": len(self._failed_models),
313
- "note": "Fallback lexical analysis available" if len(loaded) == 0 else None
 
314
  }
315
 
316
  _registry = ModelRegistry()
317
 
318
  def initialize_models(): return _registry.initialize_models()
319
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
321
- """Ensemble crypto sentiment with fallback model selection"""
322
  if not TRANSFORMERS_AVAILABLE:
323
  logger.warning("Transformers not available, using fallback")
324
  return basic_sentiment_fallback(text)
@@ -329,13 +686,20 @@ def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
329
 
330
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
331
 
332
- # Try models in order with fallback
333
- candidate_keys = ["crypto_sent_0", "crypto_sent_1", "crypto_sent_2"]
 
 
 
 
 
 
334
 
335
  for key in candidate_keys:
336
  if key not in MODEL_SPECS:
337
  continue
338
  try:
 
339
  pipe = _registry.get_pipeline(key)
340
  res = pipe(text[:512])
341
  if isinstance(res, list) and res:
@@ -356,9 +720,11 @@ def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
356
 
357
  # If we got at least one result, we can proceed
358
  if len(results) >= 1:
 
359
  break # Got at least one working model
360
 
361
- except ModelNotAvailable:
 
362
  continue # Try next model
363
  except Exception as e:
364
  logger.warning(f"Ensemble failed for {key}: {str(e)[:100]}")
@@ -491,7 +857,9 @@ def get_model_info():
491
  "social_sentiment": SOCIAL_SENTIMENT_MODELS,
492
  "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
493
  "news_sentiment": NEWS_SENTIMENT_MODELS,
494
- "decision": DECISION_MODELS
 
 
495
  },
496
  "total_models": len(MODEL_SPECS)
497
  }
 
5
  import logging
6
  import os
7
  import threading
8
+ import time
9
  from dataclasses import dataclass
10
  from typing import Any, Dict, List, Mapping, Optional, Sequence
11
  from config import HUGGINGFACE_MODELS, get_settings
 
42
  logger.warning(f"Invalid HF_MODE, resetting to 'off'")
43
 
44
  if HF_MODE == "auth" and not HF_TOKEN_ENV:
45
+ logger.error("❌ HF_MODE='auth' but no HF_TOKEN found!")
46
+ logger.error(" Please set HF_TOKEN or HUGGINGFACE_TOKEN environment variable")
47
+ logger.error(" Falling back to 'public' mode (may have rate limits)")
48
+ HF_MODE = "public" # Fallback to public instead of off
49
+
50
+ # Log authentication status
51
+ if HF_MODE != "off":
52
+ if HF_TOKEN_ENV:
53
+ logger.info(f"✅ Hugging Face token found (mode: {HF_MODE})")
54
+ else:
55
+ logger.warning(f"⚠️ No Hugging Face token found (mode: {HF_MODE}) - may hit rate limits")
56
+ else:
57
+ logger.info("ℹ️ Hugging Face mode is 'off' - using fallback only")
58
 
59
  # Linked models in HF Space - these are pre-validated
60
  LINKED_MODEL_IDS = {
 
64
  "ElKulako/cryptobert",
65
  "kk08/CryptoBERT",
66
  "agarkovv/CryptoTrader-LM",
67
+ "StephanAkkerman/FinTwitBERT-sentiment",
68
+ "OpenC/crypto-gpt-o3-mini",
69
  "burakutf/finetuned-finbert-crypto",
70
  "mathugo/crypto_news_bert",
71
  "mayurjadhav/crypto-sentiment-model",
 
74
  # Extended Model Catalog - Using VERIFIED public models only
75
  # These models are tested and confirmed working on HuggingFace Hub
76
  CRYPTO_SENTIMENT_MODELS = [
77
+ "kk08/CryptoBERT", # Crypto-specific sentiment binary classification
78
+ "ElKulako/cryptobert", # Crypto social sentiment (Bullish/Neutral/Bearish)
79
+ "cardiffnlp/twitter-roberta-base-sentiment-latest", # Fallback
80
  ]
81
  SOCIAL_SENTIMENT_MODELS = [
82
+ "ElKulako/cryptobert", # Crypto social sentiment
83
+ "cardiffnlp/twitter-roberta-base-sentiment-latest", # Twitter sentiment
84
  ]
85
  FINANCIAL_SENTIMENT_MODELS = [
86
+ "StephanAkkerman/FinTwitBERT-sentiment", # Financial tweet sentiment
87
+ "ProsusAI/finbert", # Financial sentiment
88
+ "cardiffnlp/twitter-roberta-base-sentiment-latest", # Fallback
89
  ]
90
  NEWS_SENTIMENT_MODELS = [
91
+ "StephanAkkerman/FinTwitBERT-sentiment", # News sentiment
92
+ "cardiffnlp/twitter-roberta-base-sentiment-latest", # Fallback
93
+ ]
94
+ GENERATION_MODELS = [
95
+ "OpenC/crypto-gpt-o3-mini", # Crypto/DeFi text generation
96
+ ]
97
+ TRADING_SIGNAL_MODELS = [
98
+ "agarkovv/CryptoTrader-LM", # BTC/ETH trading signals (buy/sell/hold)
99
+ ]
100
+ SUMMARIZATION_MODELS = [
101
+ "FurkanGozukara/Crypto-Financial-News-Summarizer", # Crypto/Financial news summarization
102
  ]
 
103
 
104
  @dataclass(frozen=True)
105
  class PipelineSpec:
 
121
  category="legacy"
122
  )
123
 
124
+ # Crypto sentiment - Add named keys for required models
125
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
126
+ key = f"crypto_sent_{i}"
127
+ MODEL_SPECS[key] = PipelineSpec(
128
+ key=key, task="text-classification", model_id=mid,
129
+ category="sentiment_crypto", requires_auth=("ElKulako" in mid)
130
  )
131
 
132
+ # Add specific named aliases for required models
133
+ MODEL_SPECS["crypto_sent_kk08"] = PipelineSpec(
134
+ key="crypto_sent_kk08", task="sentiment-analysis", model_id="kk08/CryptoBERT",
135
+ category="sentiment_crypto", requires_auth=False
136
+ )
137
+
138
  # Social
139
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
140
+ key = f"social_sent_{i}"
141
+ MODEL_SPECS[key] = PipelineSpec(
142
+ key=key, task="text-classification", model_id=mid,
143
+ category="sentiment_social", requires_auth=("ElKulako" in mid)
144
  )
145
 
146
+ # Add specific named alias
147
+ MODEL_SPECS["crypto_sent_social"] = PipelineSpec(
148
+ key="crypto_sent_social", task="text-classification", model_id="ElKulako/cryptobert",
149
+ category="sentiment_social", requires_auth=True
150
+ )
151
+
152
  # Financial
153
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
154
+ key = f"financial_sent_{i}"
155
+ MODEL_SPECS[key] = PipelineSpec(
156
+ key=key, task="text-classification", model_id=mid, category="sentiment_financial"
157
  )
158
 
159
+ # Add specific named alias
160
+ MODEL_SPECS["crypto_sent_fin"] = PipelineSpec(
161
+ key="crypto_sent_fin", task="sentiment-analysis", model_id="StephanAkkerman/FinTwitBERT-sentiment",
162
+ category="sentiment_financial", requires_auth=False
163
+ )
164
+
165
  # News
166
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
167
+ key = f"news_sent_{i}"
168
+ MODEL_SPECS[key] = PipelineSpec(
169
+ key=key, task="text-classification", model_id=mid, category="sentiment_news"
170
+ )
171
+
172
+ # Generation models (for crypto/DeFi text generation)
173
+ for i, mid in enumerate(GENERATION_MODELS):
174
+ key = f"crypto_gen_{i}"
175
+ MODEL_SPECS[key] = PipelineSpec(
176
+ key=key, task="text-generation", model_id=mid, category="analysis_generation"
177
+ )
178
+
179
+ # Add specific named alias
180
+ MODEL_SPECS["crypto_ai_analyst"] = PipelineSpec(
181
+ key="crypto_ai_analyst", task="text-generation", model_id="OpenC/crypto-gpt-o3-mini",
182
+ category="analysis_generation", requires_auth=False
183
+ )
184
+
185
+ # Trading signal models
186
+ for i, mid in enumerate(TRADING_SIGNAL_MODELS):
187
+ key = f"crypto_trade_{i}"
188
+ MODEL_SPECS[key] = PipelineSpec(
189
+ key=key, task="text-generation", model_id=mid, category="trading_signal"
190
+ )
191
+
192
+ # Add specific named alias
193
+ MODEL_SPECS["crypto_trading_lm"] = PipelineSpec(
194
+ key="crypto_trading_lm", task="text-generation", model_id="agarkovv/CryptoTrader-LM",
195
+ category="trading_signal", requires_auth=False
196
+ )
197
+
198
+ # Summarization models
199
+ for i, mid in enumerate(SUMMARIZATION_MODELS):
200
+ MODEL_SPECS[f"summarization_{i}"] = PipelineSpec(
201
+ key=f"summarization_{i}", task="summarization", model_id=mid, category="summarization"
202
  )
203
 
204
  class ModelNotAvailable(RuntimeError): pass
205
 
206
+ @dataclass
207
+ class ModelHealthEntry:
208
+ """Health tracking entry for a model"""
209
+ key: str
210
+ name: str
211
+ status: str = "unknown" # "healthy", "degraded", "unavailable", "unknown"
212
+ last_success: Optional[float] = None
213
+ last_error: Optional[float] = None
214
+ error_count: int = 0
215
+ success_count: int = 0
216
+ cooldown_until: Optional[float] = None
217
+ last_error_message: Optional[str] = None
218
+
219
  class ModelRegistry:
220
  def __init__(self):
221
  self._pipelines = {}
222
  self._lock = threading.Lock()
223
  self._initialized = False
224
  self._failed_models = {} # Track failed models with reasons
225
+ # Health tracking for self-healing
226
+ self._health_registry = {} # key -> health entry
227
+
228
+ def _get_or_create_health_entry(self, key: str) -> ModelHealthEntry:
229
+ """Get or create health entry for a model"""
230
+ if key not in self._health_registry:
231
+ spec = MODEL_SPECS.get(key)
232
+ self._health_registry[key] = ModelHealthEntry(
233
+ key=key,
234
+ name=spec.model_id if spec else key,
235
+ status="unknown"
236
+ )
237
+ return self._health_registry[key]
238
+
239
+ def _update_health_on_success(self, key: str):
240
+ """Update health registry after successful model call"""
241
+ entry = self._get_or_create_health_entry(key)
242
+ entry.last_success = time.time()
243
+ entry.success_count += 1
244
+
245
+ # Reset error count gradually or fully on success
246
+ if entry.error_count > 0:
247
+ entry.error_count = max(0, entry.error_count - 1)
248
+
249
+ # Recovery logic: if we have enough successes, mark as healthy
250
+ if entry.success_count >= settings.health_success_recovery_count:
251
+ entry.status = "healthy"
252
+ entry.cooldown_until = None
253
+ # Clear from failed models if present
254
+ if key in self._failed_models:
255
+ del self._failed_models[key]
256
+
257
+ def _update_health_on_failure(self, key: str, error_msg: str):
258
+ """Update health registry after failed model call"""
259
+ entry = self._get_or_create_health_entry(key)
260
+ entry.last_error = time.time()
261
+ entry.error_count += 1
262
+ entry.last_error_message = error_msg
263
+ entry.success_count = 0 # Reset success count on failure
264
+
265
+ # Determine status based on error count
266
+ if entry.error_count >= settings.health_error_threshold:
267
+ entry.status = "unavailable"
268
+ # Set cooldown period
269
+ entry.cooldown_until = time.time() + settings.health_cooldown_seconds
270
+ elif entry.error_count >= (settings.health_error_threshold // 2):
271
+ entry.status = "degraded"
272
+ else:
273
+ entry.status = "healthy"
274
+
275
+ def _is_in_cooldown(self, key: str) -> bool:
276
+ """Check if model is in cooldown period"""
277
+ if key not in self._health_registry:
278
+ return False
279
+ entry = self._health_registry[key]
280
+ if entry.cooldown_until is None:
281
+ return False
282
+ return time.time() < entry.cooldown_until
283
+
284
+ def attempt_model_reinit(self, key: str) -> Dict[str, Any]:
285
+ """
286
+ Attempt to re-initialize a failed model after cooldown.
287
+ Returns result dict with status and message.
288
+ """
289
+ if key not in MODEL_SPECS:
290
+ return {"status": "error", "message": f"Unknown model key: {key}"}
291
+
292
+ entry = self._get_or_create_health_entry(key)
293
+
294
+ # Check if enough time has passed since last error
295
+ if entry.last_error:
296
+ time_since_error = time.time() - entry.last_error
297
+ if time_since_error < settings.health_reinit_cooldown_seconds:
298
+ return {
299
+ "status": "cooldown",
300
+ "message": f"Model in cooldown, wait {int(settings.health_reinit_cooldown_seconds - time_since_error)}s",
301
+ "cooldown_remaining": int(settings.health_reinit_cooldown_seconds - time_since_error)
302
+ }
303
+
304
+ # Try to reinitialize
305
+ with self._lock:
306
+ # Remove from failed models and pipelines to force reload
307
+ if key in self._failed_models:
308
+ del self._failed_models[key]
309
+ if key in self._pipelines:
310
+ del self._pipelines[key]
311
+
312
+ # Reset health entry
313
+ entry.error_count = 0
314
+ entry.status = "unknown"
315
+ entry.cooldown_until = None
316
+
317
+ try:
318
+ # Attempt to load
319
+ pipe = self.get_pipeline(key)
320
+ return {
321
+ "status": "success",
322
+ "message": f"Model {key} successfully reinitialized",
323
+ "model": MODEL_SPECS[key].model_id
324
+ }
325
+ except Exception as e:
326
+ return {
327
+ "status": "failed",
328
+ "message": f"Reinitialization failed: {str(e)[:200]}",
329
+ "error": str(e)[:200]
330
+ }
331
+
332
+ def get_model_health_registry(self) -> List[Dict[str, Any]]:
333
+ """Get health registry for all models"""
334
+ result = []
335
+ for key, entry in self._health_registry.items():
336
+ spec = MODEL_SPECS.get(key)
337
+ result.append({
338
+ "key": entry.key,
339
+ "name": entry.name,
340
+ "model_id": spec.model_id if spec else entry.name,
341
+ "category": spec.category if spec else "unknown",
342
+ "status": entry.status,
343
+ "last_success": entry.last_success,
344
+ "last_error": entry.last_error,
345
+ "error_count": entry.error_count,
346
+ "success_count": entry.success_count,
347
+ "cooldown_until": entry.cooldown_until,
348
+ "in_cooldown": self._is_in_cooldown(key),
349
+ "last_error_message": entry.last_error_message,
350
+ "loaded": key in self._pipelines
351
+ })
352
+
353
+ # Add models that exist in specs but not in health registry
354
+ for key, spec in MODEL_SPECS.items():
355
+ if key not in self._health_registry:
356
+ result.append({
357
+ "key": key,
358
+ "name": spec.model_id,
359
+ "model_id": spec.model_id,
360
+ "category": spec.category,
361
+ "status": "unknown",
362
+ "last_success": None,
363
+ "last_error": None,
364
+ "error_count": 0,
365
+ "success_count": 0,
366
+ "cooldown_until": None,
367
+ "in_cooldown": False,
368
+ "last_error_message": None,
369
+ "loaded": key in self._pipelines
370
+ })
371
+
372
+ return result
373
 
374
  def _should_use_token(self, spec: PipelineSpec) -> Optional[str]:
375
  """Determine if and which token to use for model loading"""
 
392
  return None
393
 
394
  def get_pipeline(self, key: str):
395
+ """Get pipeline for a model key, with robust error handling and health tracking"""
396
  if HF_MODE == "off":
397
+ raise ModelNotAvailable("HF_MODE=off - models disabled")
398
  if not TRANSFORMERS_AVAILABLE:
399
+ raise ModelNotAvailable("transformers library not installed")
400
  if key not in MODEL_SPECS:
401
+ raise ModelNotAvailable(f"Unknown model key: {key}")
402
 
403
  spec = MODEL_SPECS[key]
404
 
405
+ # Check if model is in cooldown
406
+ if self._is_in_cooldown(key):
407
+ entry = self._health_registry[key]
408
+ cooldown_remaining = int(entry.cooldown_until - time.time())
409
+ raise ModelNotAvailable(f"Model in cooldown for {cooldown_remaining}s: {entry.last_error_message or 'previous failures'}")
410
+
411
+ # Return cached pipeline if available (PRIORITY: use loaded pipeline first)
412
  if key in self._pipelines:
413
+ logger.debug(f"Using cached pipeline for {key}")
414
  return self._pipelines[key]
415
 
416
  # Check if this model already failed
 
427
  # Determine token usage
428
  auth_token = self._should_use_token(spec)
429
 
430
+ logger.info(f"🔄 Loading model: {spec.model_id} (mode={HF_MODE}, auth={'' if auth_token else ''})")
431
 
432
  try:
433
  # Use token parameter instead of deprecated use_auth_token
 
439
  # Only add token if we have one and it's needed
440
  if auth_token:
441
  pipeline_kwargs["token"] = auth_token
442
+ logger.debug(f"Using authentication token for {spec.model_id}")
443
  else:
444
  # Explicitly set to None to avoid using expired tokens
445
  pipeline_kwargs["token"] = None
446
+ if HF_MODE == "auth":
447
+ logger.warning(f"No token available for {spec.model_id} in auth mode - may fail")
448
 
449
+ # Load the pipeline
450
  self._pipelines[key] = pipeline(**pipeline_kwargs)
451
+ logger.info(f"Successfully loaded model: {spec.model_id}")
452
+ # Update health on successful load
453
+ self._update_health_on_success(key)
454
  return self._pipelines[key]
455
 
456
  except RepositoryNotFoundError as e:
 
467
  if REQUESTS_AVAILABLE and isinstance(e, requests.exceptions.HTTPError):
468
  status_code = getattr(e.response, 'status_code', None)
469
  if status_code == 401:
470
+ error_msg = f"Authentication failed (401) for {spec.model_id} - check HF_TOKEN"
471
+ logger.error(f"❌ {error_msg}")
472
  elif status_code == 403:
473
  error_msg = f"Access forbidden (403) for {spec.model_id}"
474
  elif status_code == 404:
 
479
  if "not a valid model identifier" in str(e):
480
  # For linked models in HF Space, skip validation error
481
  if spec.model_id in LINKED_MODEL_IDS:
482
+ logger.info(f"Linked model {spec.model_id} - validation error ignored for linked models")
483
  # Don't mark as failed yet, it might work
484
  pass
485
  else:
486
  error_msg = f"Invalid model identifier: {spec.model_id}"
487
  elif "401" in str(e) or "403" in str(e):
488
+ error_msg = f"Authentication required for {spec.model_id} - set HF_TOKEN"
489
+ logger.error(f"❌ {error_msg}")
490
  else:
491
  error_msg = f"OS Error loading {spec.model_id}: {str(e)[:100]}"
492
 
493
+ logger.warning(f"Failed to load {spec.model_id}: {error_msg}")
494
  self._failed_models[key] = error_msg
495
+ # Update health on failure
496
+ self._update_health_on_failure(key, error_msg)
497
  raise ModelNotAvailable(error_msg) from e
498
 
499
  return self._pipelines[key]
500
+
501
+ def call_model_safe(self, key: str, text: str, **kwargs) -> Dict[str, Any]:
502
+ """
503
+ Safely call a model with health tracking.
504
+ Returns result dict with status and data or error.
505
+ """
506
+ try:
507
+ pipe = self.get_pipeline(key)
508
+ result = pipe(text[:512], **kwargs)
509
+ # Update health on successful call
510
+ self._update_health_on_success(key)
511
+ return {
512
+ "status": "success",
513
+ "data": result,
514
+ "model_key": key,
515
+ "model_id": MODEL_SPECS[key].model_id if key in MODEL_SPECS else key
516
+ }
517
+ except ModelNotAvailable as e:
518
+ # Don't update health here, already updated in get_pipeline
519
+ return {
520
+ "status": "unavailable",
521
+ "error": str(e),
522
+ "model_key": key
523
+ }
524
+ except Exception as e:
525
+ error_msg = f"{type(e).__name__}: {str(e)[:200]}"
526
+ logger.warning(f"Model call failed for {key}: {error_msg}")
527
+ # Update health on call failure
528
+ self._update_health_on_failure(key, error_msg)
529
+ return {
530
+ "status": "error",
531
+ "error": error_msg,
532
+ "model_key": key
533
+ }
534
 
535
+ def get_registry_status(self) -> Dict[str, Any]:
536
+ """Get detailed registry status with all models"""
537
+ items = []
538
+ for key, spec in MODEL_SPECS.items():
539
+ loaded = key in self._pipelines
540
+ error = self._failed_models.get(key) if key in self._failed_models else None
541
+
542
+ items.append({
543
+ "key": key,
544
+ "name": spec.model_id,
545
+ "task": spec.task,
546
+ "category": spec.category,
547
+ "loaded": loaded,
548
+ "error": error,
549
+ "requires_auth": spec.requires_auth
550
+ })
551
+
552
+ return {
553
+ "models_total": len(MODEL_SPECS),
554
+ "models_loaded": len(self._pipelines),
555
+ "models_failed": len(self._failed_models),
556
+ "items": items,
557
+ "hf_mode": HF_MODE,
558
+ "transformers_available": TRANSFORMERS_AVAILABLE,
559
+ "initialized": self._initialized
560
+ }
561
+
562
  def initialize_models(self):
563
  """Initialize models with fallback logic - tries primary models first"""
564
  if self._initialized:
 
589
  "error": "transformers library not installed - using lexical fallback"
590
  }
591
 
592
+ # Check authentication if needed
593
+ if HF_MODE == "auth" and not HF_TOKEN_ENV:
594
+ logger.error("❌ Authentication required but no token found!")
595
+ logger.error(" Set HF_TOKEN environment variable to use authenticated models")
596
+ self._initialized = True
597
+ return {
598
+ "status": "auth_required",
599
+ "mode": HF_MODE,
600
+ "models_loaded": 0,
601
+ "error": "HF_TOKEN required for auth mode",
602
+ "note": "Set HF_TOKEN environment variable"
603
+ }
604
+
605
  loaded, failed = [], []
606
 
607
  # Try to load at least one model from each category with fallback
608
  categories_to_try = {
609
+ "crypto": ["crypto_sent_0", "crypto_sent_kk08"],
610
+ "financial": ["financial_sent_0", "crypto_sent_fin"],
611
+ "social": ["social_sent_0", "crypto_sent_social"],
612
  "news": ["news_sent_0"]
613
  }
614
 
615
+ logger.info(f"🔄 Initializing models (mode: {HF_MODE}, token: {'✅' if HF_TOKEN_ENV else '❌'})...")
616
+
617
  for category, keys in categories_to_try.items():
618
  category_loaded = False
619
  for key in keys:
620
  if key not in MODEL_SPECS:
621
  continue
622
  try:
623
+ logger.info(f" Trying to load {key} ({MODEL_SPECS[key].model_id})...")
624
  self.get_pipeline(key)
625
  loaded.append(key)
626
  category_loaded = True
627
+ logger.info(f" ✅ Successfully loaded {key}")
628
  break # Successfully loaded one from this category
629
  except ModelNotAvailable as e:
630
+ error_msg = str(e)[:100]
631
+ failed.append((key, error_msg))
632
+ logger.warning(f" ❌ Failed to load {key}: {error_msg}")
633
  except Exception as e:
634
+ error_msg = f"{type(e).__name__}: {str(e)[:100]}"
635
+ failed.append((key, error_msg))
636
+ logger.error(f" ❌ Error loading {key}: {error_msg}")
637
 
638
  # Determine status - be more lenient
639
  if len(loaded) > 0:
640
  status = "ok"
641
+ logger.info(f"✅ Model initialization complete: {len(loaded)} models loaded")
642
  else:
643
  # No models loaded, but that's OK - we have fallback
644
+ logger.warning("⚠️ No HF models loaded, using fallback-only mode")
645
  status = "fallback_only"
646
 
647
  self._initialized = True
 
654
  "loaded": loaded[:10], # Limit to first 10 for brevity
655
  "failed": failed[:10], # Limit to first 10 for brevity
656
  "failed_count": len(self._failed_models),
657
+ "note": "Fallback lexical analysis available" if len(loaded) == 0 else None,
658
+ "token_available": bool(HF_TOKEN_ENV)
659
  }
660
 
661
  _registry = ModelRegistry()
662
 
663
  def initialize_models(): return _registry.initialize_models()
664
 
665
+ def get_model_health_registry() -> List[Dict[str, Any]]:
666
+ """Get health registry for all models"""
667
+ return _registry.get_model_health_registry()
668
+
669
+ def attempt_model_reinit(model_key: str) -> Dict[str, Any]:
670
+ """Attempt to re-initialize a failed model"""
671
+ return _registry.attempt_model_reinit(model_key)
672
+
673
+ def call_model_safe(model_key: str, text: str, **kwargs) -> Dict[str, Any]:
674
+ """Safely call a model with health tracking"""
675
+ return _registry.call_model_safe(model_key, text, **kwargs)
676
+
677
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
678
+ """Ensemble crypto sentiment with fallback model selection - PRIORITIZES LOADED PIPELINES"""
679
  if not TRANSFORMERS_AVAILABLE:
680
  logger.warning("Transformers not available, using fallback")
681
  return basic_sentiment_fallback(text)
 
686
 
687
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
688
 
689
+ # Try models in order with fallback - prioritize already loaded pipelines
690
+ candidate_keys = ["crypto_sent_0", "crypto_sent_kk08", "crypto_sent_1", "crypto_sent_2"]
691
+
692
+ # First, try already loaded pipelines (faster)
693
+ loaded_keys = [key for key in candidate_keys if key in _registry._pipelines]
694
+ if loaded_keys:
695
+ candidate_keys = loaded_keys + [k for k in candidate_keys if k not in loaded_keys]
696
+ logger.debug(f"Prioritizing loaded pipelines: {loaded_keys}")
697
 
698
  for key in candidate_keys:
699
  if key not in MODEL_SPECS:
700
  continue
701
  try:
702
+ # This will use cached pipeline if available, or load it if not
703
  pipe = _registry.get_pipeline(key)
704
  res = pipe(text[:512])
705
  if isinstance(res, list) and res:
 
720
 
721
  # If we got at least one result, we can proceed
722
  if len(results) >= 1:
723
+ logger.debug(f"Successfully analyzed with {key} ({spec.model_id})")
724
  break # Got at least one working model
725
 
726
+ except ModelNotAvailable as e:
727
+ logger.debug(f"Model {key} not available: {str(e)[:50]}")
728
  continue # Try next model
729
  except Exception as e:
730
  logger.warning(f"Ensemble failed for {key}: {str(e)[:100]}")
 
857
  "social_sentiment": SOCIAL_SENTIMENT_MODELS,
858
  "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
859
  "news_sentiment": NEWS_SENTIMENT_MODELS,
860
+ "generation": GENERATION_MODELS,
861
+ "trading_signals": TRADING_SIGNAL_MODELS,
862
+ "summarization": SUMMARIZATION_MODELS
863
  },
864
  "total_models": len(MODEL_SPECS)
865
  }
api_server_extended.py CHANGED
@@ -5,6 +5,7 @@ Complete Admin API with Real Data Only - NO MOCKS
5
  """
6
 
7
  import os
 
8
  import asyncio
9
  import sqlite3
10
  import httpx
@@ -207,6 +208,115 @@ def load_api_registry() -> Dict[str, Any]:
207
  return {}
208
 
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  # ===== Real Data Providers =====
211
  HEADERS = {
212
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
@@ -255,6 +365,212 @@ async def fetch_coingecko_trending() -> Dict[str, Any]:
255
  return response.json()
256
 
257
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  # ===== Lifespan Management =====
259
  @asynccontextmanager
260
  async def lifespan(app: FastAPI):
@@ -399,6 +715,25 @@ async def index():
399
  headers={"Content-Type": "text/html; charset=utf-8"}
400
  )
401
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  @app.get("/ai-tools", response_class=HTMLResponse)
403
  async def ai_tools_page(request: Request):
404
  """
@@ -425,11 +760,60 @@ async def ai_tools_page(request: Request):
425
  headers={"Content-Type": "text/html; charset=utf-8"}
426
  )
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
 
429
  # ===== Health & Status Endpoints =====
430
  @app.get("/health")
431
  async def health():
432
- """Health check endpoint"""
433
  return {
434
  "status": "healthy",
435
  "timestamp": datetime.now().isoformat(),
@@ -439,24 +823,107 @@ async def health():
439
  }
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  @app.get("/api/status")
443
  async def get_status():
444
- """System status"""
445
- config = load_providers_config()
446
- providers = config.get("providers", {})
447
-
448
- # Count by validation status
449
- validated_count = sum(1 for p in providers.values() if p.get("validated"))
450
-
451
- return {
452
- "system_health": "healthy",
453
- "timestamp": datetime.now().isoformat(),
454
- "total_providers": len(providers),
455
- "validated_providers": validated_count,
456
- "database_status": "connected",
457
- "apl_available": AUTO_DISCOVERY_REPORT_PATH.exists(),
458
- "use_mock_data": USE_MOCK_DATA
459
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
 
462
  @app.get("/api/stats")
@@ -581,8 +1048,245 @@ async def get_sentiment():
581
  raise HTTPException(status_code=503, detail=f"Failed to fetch sentiment: {str(e)}")
582
 
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  @app.get("/api/resources")
585
- async def get_resources():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  """Get resources summary for HTML dashboard (includes API registry metadata and local routes)"""
587
  try:
588
  # Load API registry for metadata
@@ -798,70 +1502,100 @@ async def get_trending():
798
  # ===== Providers Management Endpoints =====
799
  @app.get("/api/providers")
800
  async def get_providers():
801
- """Get all providers from providers_config_extended.json + HF Models + auto-discovery status"""
802
- config = load_providers_config()
803
- providers = config.get("providers", {})
804
-
805
- # Load auto-discovery report to merge status
806
- discovery_report = load_auto_discovery_report()
807
- discovery_results = {}
808
- if discovery_report and "http_providers" in discovery_report:
809
- for result in discovery_report["http_providers"].get("results", []):
810
- discovery_results[result.get("provider_id")] = result
811
-
812
- result = []
813
- for provider_id, provider_data in providers.items():
814
- # Merge with auto-discovery data if available
815
- discovery_data = discovery_results.get(provider_id, {})
816
-
817
- provider_entry = {
818
- "id": provider_id,
819
- "provider_id": provider_id, # Keep for backward compatibility
820
- "name": provider_data.get("name", provider_id),
821
- "category": provider_data.get("category", "unknown"),
822
- "base_url": provider_data.get("base_url", ""),
823
- "type": provider_data.get("type", "http"),
824
- "priority": provider_data.get("priority", 0),
825
- "weight": provider_data.get("weight", 0),
826
- "requires_auth": provider_data.get("requires_auth", False),
827
- "rate_limit": provider_data.get("rate_limit", {}),
828
- "endpoints": provider_data.get("endpoints", {}),
829
- "status": discovery_data.get("status", "UNKNOWN") if discovery_data else "unvalidated",
830
- "validated_at": provider_data.get("validated_at"),
831
- "response_time_ms": discovery_data.get("response_time_ms") or provider_data.get("response_time_ms"),
832
- "error_reason": discovery_data.get("error_reason"),
833
- "test_endpoint": discovery_data.get("test_endpoint"),
834
- "added_by": provider_data.get("added_by", "manual")
835
- }
836
- result.append(provider_entry)
837
-
838
- # Add HF Models as providers
839
  try:
840
- from ai_models import MODEL_SPECS, _registry
841
- for model_key, spec in MODEL_SPECS.items():
842
- is_loaded = model_key in _registry._pipelines
843
- result.append({
844
- "id": f"hf_model_{model_key}",
845
- "provider_id": f"hf_model_{model_key}",
846
- "name": f"HF Model: {spec.model_id}",
847
- "category": spec.category,
848
- "type": "hf_model",
849
- "status": "available" if is_loaded else "not_loaded",
850
- "model_key": model_key,
851
- "model_id": spec.model_id,
852
- "task": spec.task,
853
- "requires_auth": spec.requires_auth,
854
- "endpoint": f"/api/models/{model_key}/predict",
855
- "added_by": "hf_models"
856
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  except Exception as e:
858
- logger.warning(f"Could not add HF models as providers: {e}")
859
-
860
- return {
861
- "providers": result,
862
- "total": len(result),
863
- "source": "providers_config_extended.json + HF Models + Auto-Discovery Report"
864
- }
865
 
866
 
867
  @app.get("/api/providers/{provider_id}")
@@ -1004,6 +1738,124 @@ async def get_last_diagnostics():
1004
  }
1005
 
1006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1007
  # ===== APL (Auto Provider Loader) Endpoints =====
1008
  @app.post("/api/apl/run")
1009
  async def run_apl_scan():
@@ -1363,7 +2215,10 @@ async def analyze_sentiment(request: Dict[str, Any]):
1363
  analyze_crypto_sentiment,
1364
  analyze_financial_sentiment,
1365
  analyze_social_sentiment,
1366
- analyze_market_text
 
 
 
1367
  )
1368
 
1369
  text = request.get("text", "").strip()
@@ -1376,12 +2231,115 @@ async def analyze_sentiment(request: Dict[str, Any]):
1376
  symbol = request.get("symbol")
1377
 
1378
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1379
  if mode == "crypto":
1380
  result = analyze_crypto_sentiment(text)
1381
  elif mode == "financial":
1382
  result = analyze_financial_sentiment(text)
1383
  elif mode == "social":
1384
  result = analyze_social_sentiment(text)
 
 
 
1385
  else:
1386
  result = analyze_market_text(text)
1387
 
@@ -1389,14 +2347,17 @@ async def analyze_sentiment(request: Dict[str, Any]):
1389
  confidence = result.get("confidence", result.get("score", 0.5))
1390
  model_used = result.get("model_count", result.get("model", result.get("engine", "unknown")))
1391
 
1392
- # Prepare response compatible with ai_tools.html format
1393
  response_data = {
1394
  "ok": True,
1395
  "available": True,
 
1396
  "label": sentiment_label.lower(),
 
1397
  "score": float(confidence),
1398
  "model": f"{model_used} models" if isinstance(model_used, int) else str(model_used),
1399
- "engine": result.get("engine", "huggingface")
 
1400
  }
1401
 
1402
  # Add details if available for score bars
@@ -1445,7 +2406,9 @@ async def analyze_sentiment(request: Dict[str, Any]):
1445
  "ok": False,
1446
  "available": False,
1447
  "error": f"Analysis failed: {str(e)}",
 
1448
  "label": "neutral",
 
1449
  "score": 0.0
1450
  }
1451
 
@@ -1752,6 +2715,126 @@ async def get_latest_news(
1752
  raise HTTPException(status_code=500, detail=f"Failed to fetch news: {str(e)}")
1753
 
1754
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1755
  @app.get("/api/models/status")
1756
  async def get_models_status():
1757
  """Get AI models status and registry info - honest status reporting"""
@@ -1824,18 +2907,21 @@ async def get_models_status():
1824
  async def initialize_ai_models():
1825
  """Initialize AI models (force reload)"""
1826
  try:
1827
- from ai_models import initialize_models, registry_status
1828
 
1829
  result = initialize_models()
1830
- registry_info = registry_status()
1831
 
 
 
 
1832
  return {
1833
- "success": True,
1834
- "initialization": result,
1835
- "registry": registry_info
 
 
1836
  }
1837
- except Exception as e:
1838
- raise HTTPException(status_code=500, detail=f"Failed to initialize models: {str(e)}")
1839
 
1840
 
1841
  # ===== Model-based Data Endpoints (Using HF Models as Data Sources) =====
@@ -1843,10 +2929,22 @@ async def initialize_ai_models():
1843
  async def list_available_models():
1844
  """List all available Hugging Face models as data sources"""
1845
  try:
1846
- from ai_models import get_model_info, MODEL_SPECS, _registry, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS
1847
 
1848
  model_info = get_model_info()
1849
 
 
 
 
 
 
 
 
 
 
 
 
 
1850
  models_list = []
1851
  for key, spec in MODEL_SPECS.items():
1852
  is_loaded = key in _registry._pipelines
@@ -1855,14 +2953,16 @@ async def list_available_models():
1855
  error_msg = str(_registry._failed_models[key])
1856
 
1857
  models_list.append({
1858
- "key": key, # ai_tools.html expects "key"
1859
- "id": key, # Keep for backward compatibility
 
1860
  "model_id": spec.model_id,
1861
  "task": spec.task,
1862
  "category": spec.category,
1863
  "requires_auth": spec.requires_auth,
1864
- "loaded": is_loaded, # ai_tools.html expects "loaded" boolean
1865
- "error": error_msg, # ai_tools.html expects "error" string or null
 
1866
  "endpoint": f"/api/models/{key}/predict"
1867
  })
1868
 
@@ -1874,7 +2974,10 @@ async def list_available_models():
1874
  "crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
1875
  "social_sentiment": SOCIAL_SENTIMENT_MODELS,
1876
  "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
1877
- "news_sentiment": NEWS_SENTIMENT_MODELS
 
 
 
1878
  },
1879
  "model_info": model_info
1880
  }
@@ -2035,6 +3138,191 @@ async def batch_predict(request: Dict[str, Any]):
2035
  raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}")
2036
 
2037
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2038
  @app.get("/api/models/data/generated")
2039
  async def get_generated_data(
2040
  limit: int = 50,
 
5
  """
6
 
7
  import os
8
+ import threading
9
  import asyncio
10
  import sqlite3
11
  import httpx
 
208
  return {}
209
 
210
 
211
+ # ===== Deduplication Helpers =====
212
+ def deduplicate_providers(providers_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
213
+ """
214
+ Deduplicate providers by id, or by name+base_url if no id.
215
+ Merge tags/categories when duplicates are found.
216
+ """
217
+ seen = {}
218
+ result = []
219
+
220
+ for provider in providers_list:
221
+ # Determine unique key
222
+ provider_id = provider.get("id") or provider.get("provider_id")
223
+ if provider_id:
224
+ key = f"id:{provider_id}"
225
+ else:
226
+ name = provider.get("name", "unknown")
227
+ base_url = provider.get("base_url", "")
228
+ key = f"name_url:{name}:{base_url}"
229
+
230
+ if key in seen:
231
+ # Merge tags/categories
232
+ existing = seen[key]
233
+ existing_tags = set(existing.get("tags", []) if isinstance(existing.get("tags"), list) else [])
234
+ new_tags = set(provider.get("tags", []) if isinstance(provider.get("tags"), list) else [])
235
+ existing["tags"] = list(existing_tags | new_tags)
236
+
237
+ # Merge categories if different
238
+ existing_cat = existing.get("category", "")
239
+ new_cat = provider.get("category", "")
240
+ if new_cat and new_cat != existing_cat:
241
+ if existing_cat:
242
+ existing["categories"] = list(set([existing_cat, new_cat]))
243
+ else:
244
+ existing["category"] = new_cat
245
+ else:
246
+ # Ensure tags is a list
247
+ if "tags" not in provider:
248
+ provider["tags"] = []
249
+ elif not isinstance(provider["tags"], list):
250
+ provider["tags"] = [provider["tags"]]
251
+
252
+ seen[key] = provider
253
+ result.append(provider)
254
+
255
+ return result
256
+
257
+
258
+ def deduplicate_resources(resources_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
259
+ """
260
+ Deduplicate resources by id, or by name+url if no id.
261
+ """
262
+ seen = {}
263
+ result = []
264
+
265
+ for resource in resources_list:
266
+ # Determine unique key
267
+ resource_id = resource.get("id")
268
+ if resource_id:
269
+ key = f"id:{resource_id}"
270
+ else:
271
+ name = resource.get("name", "unknown")
272
+ url = resource.get("url") or resource.get("base_url", "")
273
+ path = resource.get("path", "")
274
+ key = f"name_url:{name}:{url}{path}"
275
+
276
+ if key not in seen:
277
+ seen[key] = resource
278
+ result.append(resource)
279
+
280
+ return result
281
+
282
+
283
+ def filter_resources_by_query(resources: List[Dict[str, Any]], query: str) -> List[Dict[str, Any]]:
284
+ """
285
+ Filter resources by search query (case-insensitive).
286
+ Searches in name, description, category, and tags.
287
+ """
288
+ if not query:
289
+ return resources
290
+
291
+ query_lower = query.lower()
292
+ filtered = []
293
+
294
+ for resource in resources:
295
+ # Search in name
296
+ if query_lower in resource.get("name", "").lower():
297
+ filtered.append(resource)
298
+ continue
299
+
300
+ # Search in description
301
+ if query_lower in resource.get("description", "").lower():
302
+ filtered.append(resource)
303
+ continue
304
+
305
+ # Search in category
306
+ if query_lower in resource.get("category", "").lower():
307
+ filtered.append(resource)
308
+ continue
309
+
310
+ # Search in tags
311
+ tags = resource.get("tags", [])
312
+ if isinstance(tags, list):
313
+ if any(query_lower in str(tag).lower() for tag in tags):
314
+ filtered.append(resource)
315
+ continue
316
+
317
+ return filtered
318
+
319
+
320
  # ===== Real Data Providers =====
321
  HEADERS = {
322
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
 
365
  return response.json()
366
 
367
 
368
+ # ===== Self-Healing Health Registry =====
369
+ from dataclasses import dataclass, field
370
+ from typing import Callable
371
+ import time as time_module
372
+
373
+ @dataclass
374
+ class ProviderHealthEntry:
375
+ """Health tracking entry for a provider/resource"""
376
+ id: str
377
+ name: str
378
+ status: str = "unknown" # "healthy", "degraded", "unavailable", "unknown"
379
+ last_success: Optional[float] = None
380
+ last_error: Optional[float] = None
381
+ error_count: int = 0
382
+ success_count: int = 0
383
+ cooldown_until: Optional[float] = None
384
+ last_error_message: Optional[str] = None
385
+
386
+ class HealthRegistry:
387
+ """
388
+ Self-healing health registry for providers and external API endpoints.
389
+ Tracks failures, implements cooldowns, and provides graceful degradation.
390
+ """
391
+ def __init__(self):
392
+ self._providers: Dict[str, ProviderHealthEntry] = {}
393
+ self._lock = threading.Lock()
394
+ # Load config
395
+ try:
396
+ from config import get_settings
397
+ self.settings = get_settings()
398
+ except:
399
+ # Fallback defaults if config not available
400
+ class FallbackSettings:
401
+ health_error_threshold = 3
402
+ health_cooldown_seconds = 300
403
+ health_success_recovery_count = 2
404
+ self.settings = FallbackSettings()
405
+
406
+ def _get_or_create_entry(self, provider_id: str, provider_name: str = None) -> ProviderHealthEntry:
407
+ """Get or create health entry for a provider"""
408
+ if provider_id not in self._providers:
409
+ self._providers[provider_id] = ProviderHealthEntry(
410
+ id=provider_id,
411
+ name=provider_name or provider_id,
412
+ status="unknown"
413
+ )
414
+ return self._providers[provider_id]
415
+
416
+ def update_on_success(self, provider_id: str, provider_name: str = None):
417
+ """Update health registry after successful provider call"""
418
+ with self._lock:
419
+ entry = self._get_or_create_entry(provider_id, provider_name)
420
+ entry.last_success = time_module.time()
421
+ entry.success_count += 1
422
+
423
+ # Reset error count gradually
424
+ if entry.error_count > 0:
425
+ entry.error_count = max(0, entry.error_count - 1)
426
+
427
+ # Recovery logic
428
+ if entry.success_count >= self.settings.health_success_recovery_count:
429
+ entry.status = "healthy"
430
+ entry.cooldown_until = None
431
+
432
+ def update_on_failure(self, provider_id: str, error_msg: str, provider_name: str = None):
433
+ """Update health registry after failed provider call"""
434
+ with self._lock:
435
+ entry = self._get_or_create_entry(provider_id, provider_name)
436
+ entry.last_error = time_module.time()
437
+ entry.error_count += 1
438
+ entry.last_error_message = error_msg[:500] # Limit error message length
439
+ entry.success_count = 0
440
+
441
+ # Determine status based on error count
442
+ if entry.error_count >= self.settings.health_error_threshold:
443
+ entry.status = "unavailable"
444
+ entry.cooldown_until = time_module.time() + self.settings.health_cooldown_seconds
445
+ elif entry.error_count >= (self.settings.health_error_threshold // 2):
446
+ entry.status = "degraded"
447
+ else:
448
+ entry.status = "healthy"
449
+
450
+ def is_in_cooldown(self, provider_id: str) -> bool:
451
+ """Check if provider is in cooldown period"""
452
+ if provider_id not in self._providers:
453
+ return False
454
+ entry = self._providers[provider_id]
455
+ if entry.cooldown_until is None:
456
+ return False
457
+ return time_module.time() < entry.cooldown_until
458
+
459
+ def get_status(self, provider_id: str) -> Optional[str]:
460
+ """Get current status of a provider"""
461
+ if provider_id not in self._providers:
462
+ return "unknown"
463
+ return self._providers[provider_id].status
464
+
465
+ def get_all_entries(self) -> List[Dict[str, Any]]:
466
+ """Get all health entries as list of dicts"""
467
+ with self._lock:
468
+ return [
469
+ {
470
+ "id": entry.id,
471
+ "name": entry.name,
472
+ "status": entry.status,
473
+ "last_success": entry.last_success,
474
+ "last_error": entry.last_error,
475
+ "error_count": entry.error_count,
476
+ "success_count": entry.success_count,
477
+ "cooldown_until": entry.cooldown_until,
478
+ "in_cooldown": self.is_in_cooldown(entry.id),
479
+ "last_error_message": entry.last_error_message
480
+ }
481
+ for entry in self._providers.values()
482
+ ]
483
+
484
+ def get_summary(self) -> Dict[str, Any]:
485
+ """Get summary statistics of health registry"""
486
+ with self._lock:
487
+ total = len(self._providers)
488
+ healthy = sum(1 for e in self._providers.values() if e.status == "healthy")
489
+ degraded = sum(1 for e in self._providers.values() if e.status == "degraded")
490
+ unavailable = sum(1 for e in self._providers.values() if e.status == "unavailable")
491
+ unknown = sum(1 for e in self._providers.values() if e.status == "unknown")
492
+ in_cooldown = sum(1 for e in self._providers.values() if self.is_in_cooldown(e.id))
493
+
494
+ return {
495
+ "total": total,
496
+ "healthy": healthy,
497
+ "degraded": degraded,
498
+ "unavailable": unavailable,
499
+ "unknown": unknown,
500
+ "in_cooldown": in_cooldown
501
+ }
502
+
503
+ # Global health registry instance
504
+ _health_registry = HealthRegistry()
505
+
506
+
507
+ async def call_provider_safe(
508
+ provider_id: str,
509
+ provider_name: str,
510
+ call_func: Callable,
511
+ *args,
512
+ **kwargs
513
+ ) -> Dict[str, Any]:
514
+ """
515
+ Safely call a provider with health tracking.
516
+
517
+ Args:
518
+ provider_id: Unique identifier for the provider
519
+ provider_name: Human-readable name
520
+ call_func: Async function to call
521
+ *args, **kwargs: Arguments to pass to call_func
522
+
523
+ Returns:
524
+ Dict with status and data or error
525
+ """
526
+ # Check if provider is in cooldown
527
+ if _health_registry.is_in_cooldown(provider_id):
528
+ entry = _health_registry._providers[provider_id]
529
+ cooldown_remaining = int(entry.cooldown_until - time_module.time())
530
+ return {
531
+ "status": "cooldown",
532
+ "error": f"Provider in cooldown for {cooldown_remaining}s",
533
+ "provider_id": provider_id,
534
+ "cooldown_remaining": cooldown_remaining
535
+ }
536
+
537
+ try:
538
+ # Call the provider function
539
+ result = await call_func(*args, **kwargs)
540
+ # Update health on success
541
+ _health_registry.update_on_success(provider_id, provider_name)
542
+ return {
543
+ "status": "success",
544
+ "data": result,
545
+ "provider_id": provider_id
546
+ }
547
+ except httpx.TimeoutException as e:
548
+ error_msg = f"Timeout: {str(e)[:200]}"
549
+ _health_registry.update_on_failure(provider_id, error_msg, provider_name)
550
+ return {
551
+ "status": "timeout",
552
+ "error": error_msg,
553
+ "provider_id": provider_id
554
+ }
555
+ except httpx.HTTPStatusError as e:
556
+ error_msg = f"HTTP {e.response.status_code}: {str(e)[:200]}"
557
+ _health_registry.update_on_failure(provider_id, error_msg, provider_name)
558
+ return {
559
+ "status": "http_error",
560
+ "error": error_msg,
561
+ "provider_id": provider_id,
562
+ "status_code": e.response.status_code
563
+ }
564
+ except Exception as e:
565
+ error_msg = f"{type(e).__name__}: {str(e)[:200]}"
566
+ _health_registry.update_on_failure(provider_id, error_msg, provider_name)
567
+ return {
568
+ "status": "error",
569
+ "error": error_msg,
570
+ "provider_id": provider_id
571
+ }
572
+
573
+
574
  # ===== Lifespan Management =====
575
  @asynccontextmanager
576
  async def lifespan(app: FastAPI):
 
715
  headers={"Content-Type": "text/html; charset=utf-8"}
716
  )
717
 
718
+ @app.get("/test.html", response_class=HTMLResponse)
719
+ async def test_page():
720
+ """Serve test.html for debugging"""
721
+ test_path = WORKSPACE_ROOT / "test.html"
722
+ if test_path.exists():
723
+ content = test_path.read_text(encoding="utf-8", errors="ignore")
724
+ return HTMLResponse(
725
+ content=content,
726
+ media_type="text/html",
727
+ headers={
728
+ "Content-Type": "text/html; charset=utf-8",
729
+ "X-Content-Type-Options": "nosniff"
730
+ }
731
+ )
732
+ return HTMLResponse(
733
+ "<h1>✅ Server is Running</h1><p>WORKSPACE_ROOT: " + str(WORKSPACE_ROOT) + "</p>",
734
+ headers={"Content-Type": "text/html; charset=utf-8"}
735
+ )
736
+
737
  @app.get("/ai-tools", response_class=HTMLResponse)
738
  async def ai_tools_page(request: Request):
739
  """
 
760
  headers={"Content-Type": "text/html; charset=utf-8"}
761
  )
762
 
763
+ @app.get("/debug-info", response_class=HTMLResponse)
764
+ async def debug_info():
765
+ """Debug endpoint to show server configuration"""
766
+ import os
767
+ info = f"""
768
+ <!DOCTYPE html>
769
+ <html>
770
+ <head>
771
+ <meta charset="UTF-8">
772
+ <title>Debug Info</title>
773
+ <style>
774
+ body {{ font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }}
775
+ .success {{ color: #0f0; }}
776
+ .error {{ color: #f00; }}
777
+ pre {{ background: #000; padding: 10px; border-radius: 5px; }}
778
+ </style>
779
+ </head>
780
+ <body>
781
+ <h1>🔍 Server Debug Information</h1>
782
+ <h2>Paths:</h2>
783
+ <pre>
784
+ WORKSPACE_ROOT: {WORKSPACE_ROOT}
785
+ Current Dir: {Path.cwd()}
786
+ index.html exists: {"✅ YES" if (WORKSPACE_ROOT / "index.html").exists() else "❌ NO"}
787
+ static dir exists: {"✅ YES" if (WORKSPACE_ROOT / "static").exists() else "❌ NO"}
788
+ </pre>
789
+ <h2>Files in WORKSPACE_ROOT:</h2>
790
+ <pre>
791
+ {chr(10).join([f"- {f.name}" for f in sorted(WORKSPACE_ROOT.glob("*.html"))[:20]])}
792
+ </pre>
793
+ <h2>Environment:</h2>
794
+ <pre>
795
+ Python: {os.sys.version}
796
+ Port: 7860
797
+ Host: 127.0.0.1
798
+ </pre>
799
+ <h2>Quick Links:</h2>
800
+ <ul>
801
+ <li><a href="/" style="color: #0ff;">Home (/)</a></li>
802
+ <li><a href="/index.html" style="color: #0ff;">Index (/index.html)</a></li>
803
+ <li><a href="/test.html" style="color: #0ff;">Test Page (/test.html)</a></li>
804
+ <li><a href="/api/health" style="color: #0ff;">API Health</a></li>
805
+ <li><a href="/docs" style="color: #0ff;">API Docs</a></li>
806
+ </ul>
807
+ </body>
808
+ </html>
809
+ """
810
+ return HTMLResponse(content=info, headers={"Content-Type": "text/html; charset=utf-8"})
811
+
812
 
813
  # ===== Health & Status Endpoints =====
814
  @app.get("/health")
815
  async def health():
816
+ """Health check endpoint (legacy)"""
817
  return {
818
  "status": "healthy",
819
  "timestamp": datetime.now().isoformat(),
 
823
  }
824
 
825
 
826
+ @app.get("/api/health")
827
+ async def api_health():
828
+ """API health check endpoint - never crashes"""
829
+ try:
830
+ version = "1.0.0"
831
+ try:
832
+ # Try to get version from metadata
833
+ api_registry = load_api_registry()
834
+ metadata = api_registry.get("metadata", {})
835
+ if metadata.get("version"):
836
+ version = metadata.get("version")
837
+ except Exception:
838
+ pass
839
+
840
+ return {
841
+ "status": "ok",
842
+ "timestamp": datetime.now().isoformat(),
843
+ "version": version
844
+ }
845
+ except Exception as e:
846
+ # Even if something goes wrong, return a clean response
847
+ logger.error(f"Health check error: {e}")
848
+ return JSONResponse(
849
+ status_code=200,
850
+ content={
851
+ "status": "ok",
852
+ "timestamp": datetime.now().isoformat(),
853
+ "version": "unknown"
854
+ }
855
+ )
856
+
857
+
858
  @app.get("/api/status")
859
  async def get_status():
860
+ """System status with real aggregated data"""
861
+ try:
862
+ # Load providers
863
+ config = load_providers_config()
864
+ providers = config.get("providers", {})
865
+
866
+ # Count free vs paid providers
867
+ free_count = sum(1 for p in providers.values()
868
+ if not p.get("requires_auth", False) and p.get("rate_limit"))
869
+ paid_count = sum(1 for p in providers.values()
870
+ if p.get("requires_auth", False))
871
+
872
+ # Load resources from unified file
873
+ resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
874
+ resources_data = {"total": 0, "categories": {}}
875
+
876
+ if resources_json.exists():
877
+ try:
878
+ with open(resources_json, 'r', encoding='utf-8') as f:
879
+ unified_data = json.load(f)
880
+ registry = unified_data.get('registry', {})
881
+
882
+ for category, items in registry.items():
883
+ if category == 'metadata':
884
+ continue
885
+ if isinstance(items, list):
886
+ count = len(items)
887
+ resources_data['total'] += count
888
+
889
+ # Group similar categories
890
+ cat_key = category.replace('_', '-')
891
+ if cat_key not in resources_data['categories']:
892
+ resources_data['categories'][cat_key] = 0
893
+ resources_data['categories'][cat_key] += count
894
+ except Exception as e:
895
+ logger.error(f"Error loading resources: {e}")
896
+
897
+ # Get model count
898
+ model_count = 0
899
+ try:
900
+ from ai_models import MODEL_SPECS
901
+ model_count = len(MODEL_SPECS) if MODEL_SPECS else 0
902
+ except Exception:
903
+ pass
904
+
905
+ return {
906
+ "status": "ok",
907
+ "timestamp": datetime.now().isoformat(),
908
+ "providers": {
909
+ "total": len(providers),
910
+ "free": free_count,
911
+ "paid": paid_count
912
+ },
913
+ "resources": resources_data,
914
+ "models": {
915
+ "total": model_count
916
+ }
917
+ }
918
+ except Exception as e:
919
+ logger.error(f"Status endpoint error: {e}")
920
+ return {
921
+ "status": "error",
922
+ "timestamp": datetime.now().isoformat(),
923
+ "error": str(e),
924
+ "providers": {"total": 0, "free": 0, "paid": 0},
925
+ "resources": {"total": 0, "categories": {}}
926
+ }
927
 
928
 
929
  @app.get("/api/stats")
 
1048
  raise HTTPException(status_code=503, detail=f"Failed to fetch sentiment: {str(e)}")
1049
 
1050
 
1051
+ @app.post("/api/sentiment")
1052
+ async def analyze_sentiment_simple(request: Dict[str, Any]):
1053
+ """Analyze sentiment with mode routing - simplified endpoint"""
1054
+ try:
1055
+ from ai_models import (
1056
+ analyze_crypto_sentiment,
1057
+ analyze_financial_sentiment,
1058
+ analyze_social_sentiment,
1059
+ _registry,
1060
+ MODEL_SPECS,
1061
+ ModelNotAvailable
1062
+ )
1063
+
1064
+ text = request.get("text", "").strip()
1065
+ if not text:
1066
+ raise HTTPException(status_code=400, detail="Text is required")
1067
+
1068
+ mode = request.get("mode", "auto").lower()
1069
+ model_key = request.get("model_key")
1070
+
1071
+ # If model_key is provided, use that specific model
1072
+ if model_key:
1073
+ if model_key not in MODEL_SPECS:
1074
+ raise HTTPException(status_code=404, detail=f"Model key '{model_key}' not found")
1075
+
1076
+ try:
1077
+ pipeline = _registry.get_pipeline(model_key)
1078
+ spec = MODEL_SPECS[model_key]
1079
+
1080
+ # Handle trading signal models specially
1081
+ if spec.category == "trading_signal":
1082
+ raw_result = pipeline(text, max_length=200, num_return_sequences=1)
1083
+ if isinstance(raw_result, list) and raw_result:
1084
+ raw_result = raw_result[0]
1085
+ generated_text = raw_result.get("generated_text", str(raw_result))
1086
+
1087
+ decision = "HOLD"
1088
+ if "buy" in generated_text.lower():
1089
+ decision = "BUY"
1090
+ elif "sell" in generated_text.lower():
1091
+ decision = "SELL"
1092
+
1093
+ return {
1094
+ "sentiment": decision.lower(),
1095
+ "confidence": 0.7,
1096
+ "raw_label": decision,
1097
+ "mode": "trading",
1098
+ "model": model_key,
1099
+ "extra": {
1100
+ "decision": decision,
1101
+ "rationale": generated_text,
1102
+ "raw": raw_result
1103
+ }
1104
+ }
1105
+
1106
+ # Regular sentiment analysis
1107
+ raw_result = pipeline(text[:512])
1108
+ if isinstance(raw_result, list) and raw_result:
1109
+ raw_result = raw_result[0]
1110
+
1111
+ label = raw_result.get("label", "neutral").upper()
1112
+ score = raw_result.get("score", 0.5)
1113
+
1114
+ # Map to standard format
1115
+ mapped = "Bullish" if "POSITIVE" in label or "BULLISH" in label or "LABEL_2" in label else (
1116
+ "Bearish" if "NEGATIVE" in label or "BEARISH" in label or "LABEL_0" in label else "Neutral"
1117
+ )
1118
+
1119
+ return {
1120
+ "sentiment": mapped,
1121
+ "confidence": score,
1122
+ "raw_label": label,
1123
+ "mode": mode,
1124
+ "model": model_key,
1125
+ "extra": {"raw": raw_result}
1126
+ }
1127
+
1128
+ except ModelNotAvailable as e:
1129
+ logger.warning(f"Model {model_key} not available: {e}")
1130
+ raise HTTPException(status_code=503, detail=f"Model not available: {str(e)}")
1131
+
1132
+ # Mode-based routing (no explicit model key)
1133
+ result = None
1134
+ actual_model = None
1135
+
1136
+ if mode == "crypto" or mode == "auto":
1137
+ result = analyze_crypto_sentiment(text)
1138
+ actual_model = "crypto_sent_kk08" # Default crypto model
1139
+ elif mode == "social":
1140
+ result = analyze_social_sentiment(text)
1141
+ actual_model = "crypto_sent_social" # ElKulako/cryptobert
1142
+ elif mode == "financial":
1143
+ result = analyze_financial_sentiment(text)
1144
+ actual_model = "crypto_sent_fin" # FinTwitBERT
1145
+ elif mode == "news":
1146
+ result = analyze_financial_sentiment(text) # Use financial for news
1147
+ actual_model = "crypto_sent_fin"
1148
+ elif mode == "trading":
1149
+ # Try to use trading model
1150
+ try:
1151
+ pipeline = _registry.get_pipeline("crypto_trading_lm")
1152
+ raw_result = pipeline(text, max_length=200, num_return_sequences=1)
1153
+ if isinstance(raw_result, list) and raw_result:
1154
+ raw_result = raw_result[0]
1155
+ generated_text = raw_result.get("generated_text", str(raw_result))
1156
+
1157
+ decision = "HOLD"
1158
+ if "buy" in generated_text.lower():
1159
+ decision = "BUY"
1160
+ elif "sell" in generated_text.lower():
1161
+ decision = "SELL"
1162
+
1163
+ return {
1164
+ "sentiment": decision,
1165
+ "confidence": 0.7,
1166
+ "raw_label": decision,
1167
+ "mode": "trading",
1168
+ "model": "crypto_trading_lm",
1169
+ "extra": {
1170
+ "decision": decision,
1171
+ "rationale": generated_text
1172
+ }
1173
+ }
1174
+ except ModelNotAvailable:
1175
+ # Fallback to crypto sentiment
1176
+ result = analyze_crypto_sentiment(text)
1177
+ actual_model = "crypto_sent_kk08"
1178
+ else:
1179
+ result = analyze_crypto_sentiment(text) # Default fallback
1180
+ actual_model = "crypto_sent_kk08"
1181
+
1182
+ if not result:
1183
+ raise HTTPException(status_code=500, detail="Sentiment analysis failed")
1184
+
1185
+ # Standardize result format
1186
+ sentiment = result.get("label", "Neutral")
1187
+ confidence = result.get("confidence", 0.5)
1188
+
1189
+ # Capitalize first letter
1190
+ sentiment_formatted = sentiment.capitalize() if isinstance(sentiment, str) else "Neutral"
1191
+
1192
+ return {
1193
+ "sentiment": sentiment_formatted,
1194
+ "confidence": confidence,
1195
+ "raw_label": sentiment,
1196
+ "mode": mode,
1197
+ "model": actual_model,
1198
+ "extra": result
1199
+ }
1200
+
1201
+ except HTTPException:
1202
+ raise
1203
+ except Exception as e:
1204
+ logger.error(f"Sentiment analysis error: {e}")
1205
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")
1206
+
1207
+
1208
  @app.get("/api/resources")
1209
+ async def get_resources(q: Optional[str] = None):
1210
+ """Get all resources with optional search query and deduplication"""
1211
+ try:
1212
+ resources_list = []
1213
+
1214
+ # Load from unified resources file
1215
+ resources_json = WORKSPACE_ROOT / "api-resources" / "crypto_resources_unified_2025-11-11.json"
1216
+ if resources_json.exists():
1217
+ try:
1218
+ with open(resources_json, 'r', encoding='utf-8') as f:
1219
+ unified_data = json.load(f)
1220
+ registry = unified_data.get('registry', {})
1221
+
1222
+ for category, items in registry.items():
1223
+ if category == 'metadata':
1224
+ continue
1225
+ if isinstance(items, list):
1226
+ for item in items:
1227
+ # Normalize resource structure
1228
+ resource = {
1229
+ "id": item.get("id"),
1230
+ "name": item.get("name", item.get("title", "Unknown")),
1231
+ "category": category,
1232
+ "url": item.get("url") or item.get("base_url", ""),
1233
+ "free": item.get("free", True),
1234
+ "auth_required": item.get("auth_required", False) or (item.get("auth", {}).get("type") != "none" if "auth" in item else False),
1235
+ "tags": item.get("tags", []) if isinstance(item.get("tags"), list) else [],
1236
+ "description": item.get("description", "") or item.get("note", "")
1237
+ }
1238
+
1239
+ # Additional fields if present
1240
+ if "method" in item:
1241
+ resource["method"] = item["method"]
1242
+ if "path" in item:
1243
+ resource["path"] = item["path"]
1244
+ if "endpoint" in item:
1245
+ resource["endpoint"] = item["endpoint"]
1246
+
1247
+ resources_list.append(resource)
1248
+ except Exception as e:
1249
+ logger.error(f"Error loading unified resources: {e}")
1250
+
1251
+ # Load from API registry (all_apis_merged_2025.json)
1252
+ api_registry = load_api_registry()
1253
+ if api_registry and "raw_files" in api_registry:
1254
+ # Parse raw files for additional resources (basic extraction)
1255
+ for raw_file in api_registry.get("raw_files", [])[:10]: # Limit to first 10
1256
+ content = raw_file.get("content", "")
1257
+ filename = raw_file.get("filename", "")
1258
+
1259
+ # Simple extraction: look for URLs in content
1260
+ import re
1261
+ urls = re.findall(r'https?://[^\s<>"]+', content)
1262
+ for url in urls[:5]: # Limit URLs per file
1263
+ resources_list.append({
1264
+ "id": None,
1265
+ "name": f"Resource from {filename}",
1266
+ "category": "discovered",
1267
+ "url": url,
1268
+ "free": True,
1269
+ "auth_required": False,
1270
+ "tags": ["auto-discovered"],
1271
+ "description": f"Auto-discovered from {filename}"
1272
+ })
1273
+
1274
+ # Apply deduplication
1275
+ deduplicated_resources = deduplicate_resources(resources_list)
1276
+
1277
+ # Apply search filter if query provided
1278
+ if q:
1279
+ deduplicated_resources = filter_resources_by_query(deduplicated_resources, q)
1280
+
1281
+ return deduplicated_resources
1282
+
1283
+ except Exception as e:
1284
+ logger.error(f"Error in get_resources: {e}")
1285
+ raise HTTPException(status_code=500, detail=f"Failed to fetch resources: {str(e)}")
1286
+
1287
+
1288
+ @app.get("/api/resources/summary")
1289
+ async def get_resources_summary():
1290
  """Get resources summary for HTML dashboard (includes API registry metadata and local routes)"""
1291
  try:
1292
  # Load API registry for metadata
 
1502
  # ===== Providers Management Endpoints =====
1503
  @app.get("/api/providers")
1504
  async def get_providers():
1505
+ """Get all providers with deduplication applied"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1506
  try:
1507
+ # Load primary config
1508
+ config = load_providers_config()
1509
+ providers_dict = config.get("providers", {})
1510
+
1511
+ # Load auto-discovery report for validation status
1512
+ discovery_report = load_auto_discovery_report()
1513
+ discovery_results = {}
1514
+ if discovery_report and "http_providers" in discovery_report:
1515
+ for result in discovery_report["http_providers"].get("results", []):
1516
+ discovery_results[result.get("provider_id")] = result
1517
+
1518
+ # Build provider list from primary config
1519
+ providers_list = []
1520
+ for provider_id, provider_data in providers_dict.items():
1521
+ # Merge with auto-discovery data if available
1522
+ discovery_data = discovery_results.get(provider_id, {})
1523
+
1524
+ # Determine auth requirement
1525
+ auth_required = provider_data.get("requires_auth", False)
1526
+ free = not auth_required
1527
+
1528
+ # Extract tags from provider data
1529
+ tags = []
1530
+ if "tags" in provider_data:
1531
+ tags = provider_data["tags"] if isinstance(provider_data["tags"], list) else [provider_data["tags"]]
1532
+
1533
+ # Build description
1534
+ description = provider_data.get("description", "") or provider_data.get("note", "")
1535
+ if not description and provider_data.get("name"):
1536
+ description = f"{provider_data.get('name')} - {provider_data.get('category', 'unknown')} provider"
1537
+
1538
+ provider_entry = {
1539
+ "id": provider_id,
1540
+ "name": provider_data.get("name", provider_id),
1541
+ "category": provider_data.get("category", "unknown"),
1542
+ "base_url": provider_data.get("base_url", ""),
1543
+ "auth_required": auth_required,
1544
+ "free": free,
1545
+ "tags": tags,
1546
+ "description": description,
1547
+ "type": provider_data.get("type", "http"),
1548
+ "priority": provider_data.get("priority", 0),
1549
+ "weight": provider_data.get("weight", 0),
1550
+ "rate_limit": provider_data.get("rate_limit", {}),
1551
+ "endpoints": provider_data.get("endpoints", {}),
1552
+ "status": discovery_data.get("status", "UNKNOWN") if discovery_data else "unvalidated",
1553
+ "validated_at": provider_data.get("validated_at"),
1554
+ "response_time_ms": discovery_data.get("response_time_ms") or provider_data.get("response_time_ms"),
1555
+ "added_by": provider_data.get("added_by", "manual")
1556
+ }
1557
+ providers_list.append(provider_entry)
1558
+
1559
+ # Add HF Models as providers (with proper structure)
1560
+ try:
1561
+ from ai_models import MODEL_SPECS, _registry
1562
+ for model_key, spec in MODEL_SPECS.items():
1563
+ is_loaded = model_key in _registry._pipelines
1564
+ providers_list.append({
1565
+ "id": f"hf_model_{model_key}",
1566
+ "name": f"HF Model: {spec.model_id}",
1567
+ "category": spec.category,
1568
+ "base_url": f"/api/models/{model_key}/predict",
1569
+ "auth_required": spec.requires_auth,
1570
+ "free": not spec.requires_auth,
1571
+ "tags": ["huggingface", "ai-model", spec.task, spec.category],
1572
+ "description": f"Hugging Face {spec.task} model for {spec.category}",
1573
+ "type": "hf_model",
1574
+ "status": "available" if is_loaded else "not_loaded",
1575
+ "model_key": model_key,
1576
+ "model_id": spec.model_id,
1577
+ "task": spec.task,
1578
+ "added_by": "hf_models"
1579
+ })
1580
+ except Exception as e:
1581
+ logger.warning(f"Could not add HF models as providers: {e}")
1582
+
1583
+ # Apply deduplication
1584
+ deduplicated_providers = deduplicate_providers(providers_list)
1585
+
1586
+ return {
1587
+ "providers": deduplicated_providers,
1588
+ "total": len(deduplicated_providers),
1589
+ "source": "providers_config_extended.json + PROVIDER_AUTO_DISCOVERY_REPORT.json + HF Models (deduplicated)"
1590
+ }
1591
  except Exception as e:
1592
+ logger.error(f"Error in get_providers: {e}")
1593
+ return {
1594
+ "providers": [],
1595
+ "total": 0,
1596
+ "error": str(e),
1597
+ "source": "error"
1598
+ }
1599
 
1600
 
1601
  @app.get("/api/providers/{provider_id}")
 
1738
  }
1739
 
1740
 
1741
+ @app.get("/api/diagnostics/health")
1742
+ async def get_diagnostics_health():
1743
+ """
1744
+ Get comprehensive health status of all providers and models.
1745
+ Returns health registry data for diagnostics and observability.
1746
+ """
1747
+ try:
1748
+ # Get provider health
1749
+ provider_health = _health_registry.get_all_entries()
1750
+ provider_summary = _health_registry.get_summary()
1751
+
1752
+ # Get model health
1753
+ model_health = []
1754
+ model_summary = {
1755
+ "total": 0,
1756
+ "healthy": 0,
1757
+ "degraded": 0,
1758
+ "unavailable": 0,
1759
+ "unknown": 0,
1760
+ "in_cooldown": 0
1761
+ }
1762
+
1763
+ try:
1764
+ from ai_models import get_model_health_registry
1765
+ model_health = get_model_health_registry()
1766
+ # Calculate model summary
1767
+ model_summary["total"] = len(model_health)
1768
+ for model in model_health:
1769
+ status = model.get("status", "unknown")
1770
+ model_summary[status] = model_summary.get(status, 0) + 1
1771
+ if model.get("in_cooldown", False):
1772
+ model_summary["in_cooldown"] += 1
1773
+ except Exception as e:
1774
+ logger.warning(f"Could not load model health: {e}")
1775
+
1776
+ return {
1777
+ "status": "success",
1778
+ "timestamp": datetime.now().isoformat(),
1779
+ "providers": {
1780
+ "summary": provider_summary,
1781
+ "entries": provider_health
1782
+ },
1783
+ "models": {
1784
+ "summary": model_summary,
1785
+ "entries": model_health
1786
+ },
1787
+ "overall_health": {
1788
+ "providers_ok": provider_summary["healthy"] >= (provider_summary["total"] // 2) if provider_summary["total"] > 0 else True,
1789
+ "models_ok": model_summary["healthy"] >= (model_summary["total"] // 4) if model_summary["total"] > 0 else True
1790
+ }
1791
+ }
1792
+ except Exception as e:
1793
+ logger.error(f"Error getting health diagnostics: {e}")
1794
+ return {
1795
+ "status": "error",
1796
+ "error": str(e),
1797
+ "timestamp": datetime.now().isoformat()
1798
+ }
1799
+
1800
+
1801
+ @app.post("/api/diagnostics/self-heal")
1802
+ async def trigger_self_heal(model_key: Optional[str] = None):
1803
+ """
1804
+ Trigger self-healing actions for models.
1805
+ Safe, idempotent, and non-blocking.
1806
+
1807
+ Query params:
1808
+ model_key: Specific model to reinitialize (optional)
1809
+ """
1810
+ try:
1811
+ from ai_models import attempt_model_reinit, get_model_health_registry
1812
+
1813
+ results = []
1814
+
1815
+ if model_key:
1816
+ # Reinit specific model
1817
+ result = attempt_model_reinit(model_key)
1818
+ results.append({
1819
+ "model_key": model_key,
1820
+ **result
1821
+ })
1822
+ else:
1823
+ # Reinit all failed models that are out of cooldown
1824
+ model_health = get_model_health_registry()
1825
+ failed_models = [
1826
+ m for m in model_health
1827
+ if m.get("status") in ["unavailable", "degraded"]
1828
+ and not m.get("in_cooldown", False)
1829
+ ]
1830
+
1831
+ for model in failed_models[:5]: # Limit to 5 at a time to avoid blocking
1832
+ result = attempt_model_reinit(model["key"])
1833
+ results.append({
1834
+ "model_key": model["key"],
1835
+ **result
1836
+ })
1837
+
1838
+ success_count = sum(1 for r in results if r.get("status") == "success")
1839
+
1840
+ return {
1841
+ "status": "completed",
1842
+ "timestamp": datetime.now().isoformat(),
1843
+ "results": results,
1844
+ "summary": {
1845
+ "total_attempts": len(results),
1846
+ "successful": success_count,
1847
+ "failed": len(results) - success_count
1848
+ }
1849
+ }
1850
+ except Exception as e:
1851
+ logger.error(f"Error in self-heal: {e}")
1852
+ return {
1853
+ "status": "error",
1854
+ "error": str(e),
1855
+ "timestamp": datetime.now().isoformat()
1856
+ }
1857
+
1858
+
1859
  # ===== APL (Auto Provider Loader) Endpoints =====
1860
  @app.post("/api/apl/run")
1861
  async def run_apl_scan():
 
2215
  analyze_crypto_sentiment,
2216
  analyze_financial_sentiment,
2217
  analyze_social_sentiment,
2218
+ analyze_market_text,
2219
+ _registry,
2220
+ MODEL_SPECS,
2221
+ ModelNotAvailable
2222
  )
2223
 
2224
  text = request.get("text", "").strip()
 
2231
  symbol = request.get("symbol")
2232
 
2233
  try:
2234
+ # If model_key is provided, use that specific model
2235
+ if model_key and model_key in MODEL_SPECS:
2236
+ try:
2237
+ pipeline = _registry.get_pipeline(model_key)
2238
+ spec = MODEL_SPECS[model_key]
2239
+
2240
+ # Handle different task types
2241
+ if spec.task == "text-generation":
2242
+ # For trading signal models or generation models
2243
+ raw_result = pipeline(text, max_length=200, num_return_sequences=1)
2244
+ if isinstance(raw_result, list) and raw_result:
2245
+ raw_result = raw_result[0]
2246
+
2247
+ generated_text = raw_result.get("generated_text", str(raw_result))
2248
+
2249
+ # Parse trading signals if applicable
2250
+ if spec.category == "trading_signal":
2251
+ # Extract signal from generated text
2252
+ decision = "HOLD"
2253
+ if "buy" in generated_text.lower():
2254
+ decision = "BUY"
2255
+ elif "sell" in generated_text.lower():
2256
+ decision = "SELL"
2257
+
2258
+ return {
2259
+ "ok": True,
2260
+ "available": True,
2261
+ "sentiment": decision.lower(),
2262
+ "label": decision.lower(),
2263
+ "score": 0.7,
2264
+ "confidence": 0.7,
2265
+ "model": model_key,
2266
+ "engine": "huggingface",
2267
+ "mode": "trading",
2268
+ "extra": {
2269
+ "decision": decision,
2270
+ "rationale": generated_text,
2271
+ "raw": raw_result
2272
+ }
2273
+ }
2274
+ else:
2275
+ # Generation model - return generated text
2276
+ return {
2277
+ "ok": True,
2278
+ "available": True,
2279
+ "sentiment": "neutral",
2280
+ "label": "neutral",
2281
+ "score": 0.5,
2282
+ "confidence": 0.5,
2283
+ "model": model_key,
2284
+ "engine": "huggingface",
2285
+ "mode": "generation",
2286
+ "extra": {
2287
+ "generated_text": generated_text,
2288
+ "raw": raw_result
2289
+ }
2290
+ }
2291
+ else:
2292
+ # Text classification / sentiment
2293
+ raw_result = pipeline(text[:512])
2294
+ if isinstance(raw_result, list) and raw_result:
2295
+ raw_result = raw_result[0]
2296
+
2297
+ label = raw_result.get("label", "neutral").upper()
2298
+ score = raw_result.get("score", 0.5)
2299
+
2300
+ # Map labels to standard format
2301
+ mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label or "LABEL_2" in label else (
2302
+ "bearish" if "NEGATIVE" in label or "BEARISH" in label or "LABEL_0" in label else "neutral"
2303
+ )
2304
+
2305
+ return {
2306
+ "ok": True,
2307
+ "available": True,
2308
+ "sentiment": mapped,
2309
+ "label": mapped,
2310
+ "score": score,
2311
+ "confidence": score,
2312
+ "raw_label": label,
2313
+ "model": model_key,
2314
+ "engine": "huggingface",
2315
+ "mode": mode,
2316
+ "extra": {
2317
+ "vote": score if mapped == "bullish" else (-score if mapped == "bearish" else 0.0),
2318
+ "raw": raw_result
2319
+ }
2320
+ }
2321
+ except ModelNotAvailable as e:
2322
+ logger.warning(f"Model {model_key} not available: {e}")
2323
+ return {
2324
+ "ok": False,
2325
+ "available": False,
2326
+ "error": f"Model {model_key} not available: {str(e)}",
2327
+ "label": "neutral",
2328
+ "sentiment": "neutral",
2329
+ "score": 0.0,
2330
+ "confidence": 0.0
2331
+ }
2332
+
2333
+ # Default mode-based analysis
2334
  if mode == "crypto":
2335
  result = analyze_crypto_sentiment(text)
2336
  elif mode == "financial":
2337
  result = analyze_financial_sentiment(text)
2338
  elif mode == "social":
2339
  result = analyze_social_sentiment(text)
2340
+ elif mode == "trading":
2341
+ # Try to use trading signal model
2342
+ result = analyze_crypto_sentiment(text)
2343
  else:
2344
  result = analyze_market_text(text)
2345
 
 
2347
  confidence = result.get("confidence", result.get("score", 0.5))
2348
  model_used = result.get("model_count", result.get("model", result.get("engine", "unknown")))
2349
 
2350
+ # Prepare response compatible with frontend format
2351
  response_data = {
2352
  "ok": True,
2353
  "available": True,
2354
+ "sentiment": sentiment_label.lower(),
2355
  "label": sentiment_label.lower(),
2356
+ "confidence": float(confidence),
2357
  "score": float(confidence),
2358
  "model": f"{model_used} models" if isinstance(model_used, int) else str(model_used),
2359
+ "engine": result.get("engine", "huggingface"),
2360
+ "mode": mode
2361
  }
2362
 
2363
  # Add details if available for score bars
 
2406
  "ok": False,
2407
  "available": False,
2408
  "error": f"Analysis failed: {str(e)}",
2409
+ "sentiment": "neutral",
2410
  "label": "neutral",
2411
+ "confidence": 0.0,
2412
  "score": 0.0
2413
  }
2414
 
 
2715
  raise HTTPException(status_code=500, detail=f"Failed to fetch news: {str(e)}")
2716
 
2717
 
2718
+ @app.post("/api/news/summarize")
2719
+ async def summarize_news(request: Dict[str, Any]):
2720
+ """
2721
+ Summarize crypto/financial news using Hugging Face Crypto-Financial-News-Summarizer model
2722
+
2723
+ Expects: { "title": "News Title", "content": "Full article text" }
2724
+ Returns: { "summary": "Summarized news paragraph", "model": "Crypto-Financial-News-Summarizer" }
2725
+ """
2726
+ try:
2727
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
2728
+
2729
+ title = request.get("title", "").strip()
2730
+ content = request.get("content", "").strip()
2731
+
2732
+ if not title and not content:
2733
+ raise HTTPException(status_code=400, detail="Title or content is required")
2734
+
2735
+ # Combine title and content for summarization
2736
+ text_to_summarize = f"{title}. {content}" if title and content else (title or content)
2737
+
2738
+ try:
2739
+ # Try to use the Crypto-Financial-News-Summarizer model
2740
+ summarization_key = "summarization_0"
2741
+
2742
+ if summarization_key in MODEL_SPECS:
2743
+ try:
2744
+ pipeline = _registry.get_pipeline(summarization_key)
2745
+ spec = MODEL_SPECS[summarization_key]
2746
+
2747
+ # Use HF model for summarization
2748
+ # Limit input text to avoid token length issues
2749
+ max_input_length = 1024
2750
+ text_input = text_to_summarize[:max_input_length]
2751
+
2752
+ try:
2753
+ # Try with parameters first
2754
+ summary_result = pipeline(
2755
+ text_input,
2756
+ max_length=150,
2757
+ min_length=50,
2758
+ do_sample=False,
2759
+ truncation=True
2760
+ )
2761
+ except TypeError:
2762
+ # Some pipelines don't accept these parameters
2763
+ summary_result = pipeline(text_input, truncation=True)
2764
+
2765
+ # Extract summary text from result
2766
+ if isinstance(summary_result, list) and summary_result:
2767
+ summary_text = summary_result[0].get("summary_text", summary_result[0].get("generated_text", str(summary_result[0])))
2768
+ elif isinstance(summary_result, dict):
2769
+ summary_text = summary_result.get("summary_text", summary_result.get("generated_text", str(summary_result)))
2770
+ else:
2771
+ summary_text = str(summary_result)
2772
+
2773
+ return {
2774
+ "success": True,
2775
+ "summary": summary_text,
2776
+ "model": spec.model_id,
2777
+ "available": True,
2778
+ "input_length": len(text_input),
2779
+ "title": title,
2780
+ "timestamp": datetime.now().isoformat()
2781
+ }
2782
+
2783
+ except ModelNotAvailable as e:
2784
+ logger.warning(f"Crypto-Financial-News-Summarizer not available: {e}")
2785
+ # Fall through to fallback
2786
+ except Exception as e:
2787
+ logger.warning(f"HF summarization failed: {e}, using fallback")
2788
+ # Fall through to fallback
2789
+
2790
+ # Fallback: Simple extractive summarization
2791
+ # Split into sentences and take the most important ones
2792
+ sentences = []
2793
+ current_sentence = ""
2794
+
2795
+ for char in text_to_summarize:
2796
+ current_sentence += char
2797
+ if char in ".!?":
2798
+ sentence = current_sentence.strip()
2799
+ if sentence and len(sentence) > 10: # Filter out very short sentences
2800
+ sentences.append(sentence)
2801
+ current_sentence = ""
2802
+ if len(sentences) >= 5: # Take first 5 sentences max
2803
+ break
2804
+
2805
+ # If we didn't get enough sentences, add the rest
2806
+ if len(sentences) < 3 and current_sentence.strip():
2807
+ sentences.append(current_sentence.strip())
2808
+
2809
+ # Take first 3 sentences as summary
2810
+ summary = " ".join(sentences[:3]) if sentences else text_to_summarize[:500]
2811
+
2812
+ return {
2813
+ "success": True,
2814
+ "summary": summary,
2815
+ "model": "fallback_extractive",
2816
+ "available": False,
2817
+ "note": "Using fallback extractive summarization (HF model not available)",
2818
+ "title": title,
2819
+ "timestamp": datetime.now().isoformat()
2820
+ }
2821
+
2822
+ except Exception as e:
2823
+ logger.error(f"Summarization error: {str(e)}")
2824
+ return {
2825
+ "success": False,
2826
+ "error": f"Summarization failed: {str(e)}",
2827
+ "summary": "",
2828
+ "model": "error",
2829
+ "available": False
2830
+ }
2831
+
2832
+ except HTTPException:
2833
+ raise
2834
+ except Exception as e:
2835
+ raise HTTPException(status_code=500, detail=f"News summarization failed: {str(e)}")
2836
+
2837
+
2838
  @app.get("/api/models/status")
2839
  async def get_models_status():
2840
  """Get AI models status and registry info - honest status reporting"""
 
2907
  async def initialize_ai_models():
2908
  """Initialize AI models (force reload)"""
2909
  try:
2910
+ from ai_models import initialize_models, _registry
2911
 
2912
  result = initialize_models()
2913
+ registry_status = _registry.get_registry_status()
2914
 
2915
+ return registry_status
2916
+ except Exception as e:
2917
+ logger.error(f"Failed to initialize models: {e}")
2918
  return {
2919
+ "models_total": 0,
2920
+ "models_loaded": 0,
2921
+ "models_failed": 0,
2922
+ "items": [],
2923
+ "error": str(e)
2924
  }
 
 
2925
 
2926
 
2927
  # ===== Model-based Data Endpoints (Using HF Models as Data Sources) =====
 
2929
  async def list_available_models():
2930
  """List all available Hugging Face models as data sources"""
2931
  try:
2932
+ from ai_models import get_model_info, MODEL_SPECS, _registry, CRYPTO_SENTIMENT_MODELS, SOCIAL_SENTIMENT_MODELS, FINANCIAL_SENTIMENT_MODELS, NEWS_SENTIMENT_MODELS, GENERATION_MODELS, TRADING_SIGNAL_MODELS
2933
 
2934
  model_info = get_model_info()
2935
 
2936
+ # Model descriptions
2937
+ model_descriptions = {
2938
+ "kk08/CryptoBERT": "Crypto sentiment binary classification model trained on cryptocurrency-related text",
2939
+ "ElKulako/cryptobert": "Crypto social sentiment classifier (Bullish/Neutral/Bearish) for social media and news",
2940
+ "StephanAkkerman/FinTwitBERT-sentiment": "Financial tweet sentiment analysis model for market-related social media content",
2941
+ "OpenC/crypto-gpt-o3-mini": "Crypto and DeFi text generation model for analysis and content creation",
2942
+ "agarkovv/CryptoTrader-LM": "BTC/ETH trading signal generator providing daily buy/sell/hold recommendations",
2943
+ "cardiffnlp/twitter-roberta-base-sentiment-latest": "General Twitter sentiment analysis (fallback model)",
2944
+ "ProsusAI/finbert": "Financial sentiment analysis model for news and financial documents",
2945
+ "FurkanGozukara/Crypto-Financial-News-Summarizer": "Specialized model for summarizing cryptocurrency and financial news articles"
2946
+ }
2947
+
2948
  models_list = []
2949
  for key, spec in MODEL_SPECS.items():
2950
  is_loaded = key in _registry._pipelines
 
2953
  error_msg = str(_registry._failed_models[key])
2954
 
2955
  models_list.append({
2956
+ "key": key,
2957
+ "id": key,
2958
+ "name": spec.model_id,
2959
  "model_id": spec.model_id,
2960
  "task": spec.task,
2961
  "category": spec.category,
2962
  "requires_auth": spec.requires_auth,
2963
+ "loaded": is_loaded,
2964
+ "error": error_msg,
2965
+ "description": model_descriptions.get(spec.model_id, f"{spec.category} model for {spec.task}"),
2966
  "endpoint": f"/api/models/{key}/predict"
2967
  })
2968
 
 
2974
  "crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
2975
  "social_sentiment": SOCIAL_SENTIMENT_MODELS,
2976
  "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
2977
+ "news_sentiment": NEWS_SENTIMENT_MODELS,
2978
+ "generation": GENERATION_MODELS,
2979
+ "trading_signals": TRADING_SIGNAL_MODELS,
2980
+ "summarization": ["FurkanGozukara/Crypto-Financial-News-Summarizer"]
2981
  },
2982
  "model_info": model_info
2983
  }
 
3138
  raise HTTPException(status_code=500, detail=f"Batch prediction failed: {str(e)}")
3139
 
3140
 
3141
+ @app.post("/api/analyze/text")
3142
+ async def analyze_text(request: Dict[str, Any]):
3143
+ """
3144
+ Analyze or generate text using crypto-gpt-o3-mini generation model.
3145
+
3146
+ Expects: { "prompt": "...", "mode": "analysis" | "generation" }
3147
+ Returns: { "text": "...", "model": "OpenC/crypto-gpt-o3-mini" }
3148
+ """
3149
+ try:
3150
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
3151
+
3152
+ prompt = request.get("prompt", "").strip()
3153
+ mode = request.get("mode", "analysis").lower()
3154
+ max_length = request.get("max_length", 200)
3155
+
3156
+ if not prompt:
3157
+ raise HTTPException(status_code=400, detail="Prompt is required")
3158
+
3159
+ # Find generation model (crypto-gpt-o3-mini) - use specific key first
3160
+ generation_key = "crypto_ai_analyst" if "crypto_ai_analyst" in MODEL_SPECS else None
3161
+
3162
+ # Fallback: search by category or model name
3163
+ if not generation_key:
3164
+ for key, spec in MODEL_SPECS.items():
3165
+ if spec.category == "analysis_generation" or "crypto-gpt" in spec.model_id.lower():
3166
+ generation_key = key
3167
+ break
3168
+
3169
+ if not generation_key:
3170
+ return {
3171
+ "success": False,
3172
+ "available": False,
3173
+ "error": "Crypto text generation model not configured",
3174
+ "text": ""
3175
+ }
3176
+
3177
+ try:
3178
+ spec = MODEL_SPECS[generation_key]
3179
+ pipeline = _registry.get_pipeline(generation_key)
3180
+
3181
+ # Generate text
3182
+ result = pipeline(prompt, max_length=max_length, num_return_sequences=1, truncation=True)
3183
+
3184
+ if isinstance(result, list) and result:
3185
+ result = result[0]
3186
+
3187
+ generated_text = result.get("generated_text", str(result))
3188
+
3189
+ return {
3190
+ "success": True,
3191
+ "available": True,
3192
+ "text": generated_text,
3193
+ "model": spec.model_id,
3194
+ "mode": mode,
3195
+ "prompt": prompt[:100],
3196
+ "timestamp": datetime.now().isoformat()
3197
+ }
3198
+
3199
+ except ModelNotAvailable as e:
3200
+ logger.warning(f"Generation model not available: {e}")
3201
+ return {
3202
+ "success": False,
3203
+ "available": False,
3204
+ "error": f"Model not available: {str(e)}",
3205
+ "text": "",
3206
+ "note": "HF model unavailable - check model configuration"
3207
+ }
3208
+
3209
+ except HTTPException:
3210
+ raise
3211
+ except Exception as e:
3212
+ logger.error(f"Text analysis failed: {e}")
3213
+ raise HTTPException(status_code=500, detail=f"Text analysis failed: {str(e)}")
3214
+
3215
+
3216
+ @app.post("/api/trading/decision")
3217
+ async def trading_decision(request: Dict[str, Any]):
3218
+ """
3219
+ Get trading decision from CryptoTrader-LM model.
3220
+
3221
+ Expects: { "symbol": "BTC", "context": "market context..." }
3222
+ Returns: {
3223
+ "decision": "BUY" | "SELL" | "HOLD",
3224
+ "confidence": float,
3225
+ "rationale": "explanation",
3226
+ "raw": {...}
3227
+ }
3228
+ """
3229
+ try:
3230
+ from ai_models import MODEL_SPECS, _registry, ModelNotAvailable
3231
+
3232
+ symbol = request.get("symbol", "").strip().upper()
3233
+ context = request.get("context", "").strip()
3234
+
3235
+ if not symbol:
3236
+ raise HTTPException(status_code=400, detail="Symbol is required")
3237
+
3238
+ # Find trading signal model (CryptoTrader-LM) - use specific key first
3239
+ trading_key = "crypto_trading_lm" if "crypto_trading_lm" in MODEL_SPECS else None
3240
+
3241
+ # Fallback: search by category or model name
3242
+ if not trading_key:
3243
+ for key, spec in MODEL_SPECS.items():
3244
+ if spec.category == "trading_signal" or "cryptotrader" in spec.model_id.lower():
3245
+ trading_key = key
3246
+ break
3247
+
3248
+ if not trading_key:
3249
+ return {
3250
+ "success": False,
3251
+ "available": False,
3252
+ "error": "Trading signal model not configured",
3253
+ "decision": "HOLD",
3254
+ "confidence": 0.0,
3255
+ "rationale": "Model not available"
3256
+ }
3257
+
3258
+ try:
3259
+ spec = MODEL_SPECS[trading_key]
3260
+ pipeline = _registry.get_pipeline(trading_key)
3261
+
3262
+ # Build prompt for trading model
3263
+ if context:
3264
+ prompt = f"Trading analysis for {symbol}: {context}"
3265
+ else:
3266
+ prompt = f"Trading signal for {symbol}"
3267
+
3268
+ # Generate trading signal
3269
+ result = pipeline(prompt, max_length=150, num_return_sequences=1, truncation=True)
3270
+
3271
+ if isinstance(result, list) and result:
3272
+ result = result[0]
3273
+
3274
+ generated_text = result.get("generated_text", str(result))
3275
+
3276
+ # Parse decision from generated text
3277
+ decision = "HOLD" # Default
3278
+ confidence = 0.5
3279
+
3280
+ text_lower = generated_text.lower()
3281
+ if "buy" in text_lower and "sell" not in text_lower:
3282
+ decision = "BUY"
3283
+ confidence = 0.7
3284
+ elif "sell" in text_lower and "buy" not in text_lower:
3285
+ decision = "SELL"
3286
+ confidence = 0.7
3287
+ elif "hold" in text_lower:
3288
+ decision = "HOLD"
3289
+ confidence = 0.6
3290
+
3291
+ # Extract rationale (use generated text as rationale)
3292
+ rationale = generated_text if len(generated_text) < 500 else generated_text[:497] + "..."
3293
+
3294
+ return {
3295
+ "success": True,
3296
+ "available": True,
3297
+ "decision": decision,
3298
+ "confidence": confidence,
3299
+ "rationale": rationale,
3300
+ "symbol": symbol,
3301
+ "model": spec.model_id,
3302
+ "context_provided": bool(context),
3303
+ "raw": result,
3304
+ "timestamp": datetime.now().isoformat()
3305
+ }
3306
+
3307
+ except ModelNotAvailable as e:
3308
+ logger.warning(f"Trading model not available: {e}")
3309
+ return {
3310
+ "success": False,
3311
+ "available": False,
3312
+ "error": f"Model not available: {str(e)}",
3313
+ "decision": "HOLD",
3314
+ "confidence": 0.0,
3315
+ "rationale": "Trading model unavailable - check configuration",
3316
+ "note": "HF model unavailable"
3317
+ }
3318
+
3319
+ except HTTPException:
3320
+ raise
3321
+ except Exception as e:
3322
+ logger.error(f"Trading decision failed: {e}")
3323
+ raise HTTPException(status_code=500, detail=f"Trading decision failed: {str(e)}")
3324
+
3325
+
3326
  @app.get("/api/models/data/generated")
3327
  async def get_generated_data(
3328
  limit: int = 50,
app.py CHANGED
@@ -78,6 +78,10 @@ class CryptoDataHub:
78
  data = json.load(f)
79
  self.resources['unified'] = data
80
  logger.info(f"✅ Loaded unified resources: {RESOURCES_JSON}")
 
 
 
 
81
 
82
  # Load all APIs merged
83
  if ALL_APIS_JSON.exists():
@@ -85,10 +89,70 @@ class CryptoDataHub:
85
  data = json.load(f)
86
  self.resources['all_apis'] = data
87
  logger.info(f"✅ Loaded all APIs: {ALL_APIS_JSON}")
 
 
 
 
88
 
89
  logger.info(f"📊 Total resource files loaded: {len(self.resources)}")
90
  except Exception as e:
91
  logger.error(f"❌ Error loading resources: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  def initialize_models(self):
94
  """بارگذاری مدل‌های Hugging Face"""
 
78
  data = json.load(f)
79
  self.resources['unified'] = data
80
  logger.info(f"✅ Loaded unified resources: {RESOURCES_JSON}")
81
+ else:
82
+ # Fallback data structure
83
+ logger.warning(f"⚠️ Resources JSON not found at {RESOURCES_JSON}, using fallback data")
84
+ self.resources['unified'] = self._get_fallback_unified_resources()
85
 
86
  # Load all APIs merged
87
  if ALL_APIS_JSON.exists():
 
89
  data = json.load(f)
90
  self.resources['all_apis'] = data
91
  logger.info(f"✅ Loaded all APIs: {ALL_APIS_JSON}")
92
+ else:
93
+ # Fallback data structure
94
+ logger.warning(f"⚠️ All APIs JSON not found at {ALL_APIS_JSON}, using fallback data")
95
+ self.resources['all_apis'] = self._get_fallback_apis_data()
96
 
97
  logger.info(f"📊 Total resource files loaded: {len(self.resources)}")
98
  except Exception as e:
99
  logger.error(f"❌ Error loading resources: {e}")
100
+ # Use fallback data on error
101
+ if 'unified' not in self.resources:
102
+ self.resources['unified'] = self._get_fallback_unified_resources()
103
+ if 'all_apis' not in self.resources:
104
+ self.resources['all_apis'] = self._get_fallback_apis_data()
105
+
106
+ def _get_fallback_unified_resources(self) -> Dict:
107
+ """Fallback unified resources structure"""
108
+ return {
109
+ "metadata": {
110
+ "name": "Crypto Resources (Fallback)",
111
+ "version": "1.0.0",
112
+ "generated_at": datetime.now().isoformat(),
113
+ "source": "fallback"
114
+ },
115
+ "registry": {
116
+ "market_data": [
117
+ {
118
+ "name": "CoinGecko",
119
+ "base_url": "https://api.coingecko.com/api/v3",
120
+ "free": True,
121
+ "auth": {},
122
+ "description": "Free cryptocurrency market data API"
123
+ },
124
+ {
125
+ "name": "Binance Public",
126
+ "base_url": "https://api.binance.com/api/v3",
127
+ "free": True,
128
+ "auth": {},
129
+ "description": "Binance public market data API"
130
+ }
131
+ ],
132
+ "news": [
133
+ {
134
+ "name": "CryptoCompare News",
135
+ "base_url": "https://min-api.cryptocompare.com/data/v2",
136
+ "free": True,
137
+ "auth": {},
138
+ "description": "Cryptocurrency news API"
139
+ }
140
+ ]
141
+ }
142
+ }
143
+
144
+ def _get_fallback_apis_data(self) -> Dict:
145
+ """Fallback APIs data structure"""
146
+ return {
147
+ "metadata": {
148
+ "name": "Crypto APIs (Fallback)",
149
+ "version": "1.0.0",
150
+ "generated_at": datetime.now().isoformat(),
151
+ "source": "fallback"
152
+ },
153
+ "discovered_keys": {},
154
+ "raw_files": []
155
+ }
156
 
157
  def initialize_models(self):
158
  """بارگذاری مدل‌های Hugging Face"""
backend/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/__pycache__/__init__.cpython-313.pyc and b/backend/__pycache__/__init__.cpython-313.pyc differ
 
backend/services/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/__init__.cpython-313.pyc and b/backend/services/__pycache__/__init__.cpython-313.pyc differ
 
backend/services/__pycache__/resource_validator.cpython-313.pyc ADDED
Binary file (10.3 kB). View file
 
config.py CHANGED
@@ -11,10 +11,25 @@ HUGGINGFACE_MODELS: Dict[str, str] = {
11
  "crypto_sentiment": "ElKulako/cryptobert",
12
  }
13
 
 
 
 
 
 
 
 
 
 
14
  class Settings:
15
  """Application settings."""
16
  def __init__(self):
17
  self.hf_token: Optional[str] = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
 
 
 
 
 
 
18
 
19
  _settings = Settings()
20
 
 
11
  "crypto_sentiment": "ElKulako/cryptobert",
12
  }
13
 
14
+ # Self-Healing Configuration
15
+ SELF_HEALING_CONFIG = {
16
+ "error_threshold": int(os.getenv("HEALTH_ERROR_THRESHOLD", "3")), # Failures before degraded
17
+ "cooldown_seconds": int(os.getenv("HEALTH_COOLDOWN_SECONDS", "300")), # 5 minutes default
18
+ "success_recovery_count": int(os.getenv("HEALTH_RECOVERY_COUNT", "2")), # Successes to recover
19
+ "enable_auto_reinit": os.getenv("HEALTH_AUTO_REINIT", "true").lower() == "true",
20
+ "reinit_cooldown_seconds": int(os.getenv("HEALTH_REINIT_COOLDOWN", "600")), # 10 minutes
21
+ }
22
+
23
  class Settings:
24
  """Application settings."""
25
  def __init__(self):
26
  self.hf_token: Optional[str] = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN")
27
+ # Self-healing settings
28
+ self.health_error_threshold: int = SELF_HEALING_CONFIG["error_threshold"]
29
+ self.health_cooldown_seconds: int = SELF_HEALING_CONFIG["cooldown_seconds"]
30
+ self.health_success_recovery_count: int = SELF_HEALING_CONFIG["success_recovery_count"]
31
+ self.health_enable_auto_reinit: bool = SELF_HEALING_CONFIG["enable_auto_reinit"]
32
+ self.health_reinit_cooldown_seconds: int = SELF_HEALING_CONFIG["reinit_cooldown_seconds"]
33
 
34
  _settings = Settings()
35
 
crypto_resources_unified_2025-11-11.json CHANGED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -17,6 +17,8 @@
17
 
18
  <!-- Styles -->
19
  <link rel="stylesheet" href="/static/css/main.css">
 
 
20
  <script src="/static/js/trading-pairs-loader.js" defer></script>
21
  <script src="/static/js/app.js" defer></script>
22
 
@@ -24,6 +26,9 @@
24
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
25
  </head>
26
  <body>
 
 
 
27
  <div class="app-container">
28
  <!-- Header -->
29
  <header class="app-header">
@@ -79,6 +84,14 @@
79
  <i class="fas fa-brain"></i>
80
  <span>Sentiment</span>
81
  </button>
 
 
 
 
 
 
 
 
82
  <button class="tab-btn" data-tab="news">
83
  <i class="fas fa-newspaper"></i>
84
  <span>News</span>
@@ -270,12 +283,131 @@
270
  <div id="news-sentiment-result" style="margin-top: 20px;"></div>
271
  </div>
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  <div class="card">
274
  <h3>Analysis History</h3>
275
  <div id="sentiment-history"></div>
276
  </div>
277
  </section>
278
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  <!-- News Tab -->
280
  <section id="tab-news" class="tab-content">
281
  <div class="section-header">
@@ -315,6 +447,12 @@
315
  <button class="btn-primary" onclick="runDiagnostics()">▶️ Run Diagnostics</button>
316
  </div>
317
 
 
 
 
 
 
 
318
  <div class="card">
319
  <h3>System Status</h3>
320
  <div id="diagnostics-status"></div>
 
17
 
18
  <!-- Styles -->
19
  <link rel="stylesheet" href="/static/css/main.css">
20
+ <link rel="stylesheet" href="/static/css/toast.css">
21
+ <link rel="stylesheet" href="/static/css/enhancements.css">
22
  <script src="/static/js/trading-pairs-loader.js" defer></script>
23
  <script src="/static/js/app.js" defer></script>
24
 
 
26
  <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🚀</text></svg>">
27
  </head>
28
  <body>
29
+ <!-- Toast Container -->
30
+ <div class="toast-container" id="toast-container"></div>
31
+
32
  <div class="app-container">
33
  <!-- Header -->
34
  <header class="app-header">
 
84
  <i class="fas fa-brain"></i>
85
  <span>Sentiment</span>
86
  </button>
87
+ <button class="tab-btn" data-tab="ai-analyst">
88
+ <i class="fas fa-magic"></i>
89
+ <span>AI Analyst</span>
90
+ </button>
91
+ <button class="tab-btn" data-tab="trading-assistant">
92
+ <i class="fas fa-chart-bar"></i>
93
+ <span>Trading Signals</span>
94
+ </button>
95
  <button class="tab-btn" data-tab="news">
96
  <i class="fas fa-newspaper"></i>
97
  <span>News</span>
 
283
  <div id="news-sentiment-result" style="margin-top: 20px;"></div>
284
  </div>
285
 
286
+ <!-- News Summarization -->
287
+ <div class="card">
288
+ <h3>📝 News Summarization</h3>
289
+ <p style="color: var(--text-secondary); margin-bottom: 15px;">
290
+ Summarize crypto/financial news using AI-powered Hugging Face model
291
+ </p>
292
+ <div class="form-group">
293
+ <label>News Title:</label>
294
+ <input type="text" id="summary-news-title" placeholder="Example: Bitcoin Reaches New All-Time High">
295
+ </div>
296
+ <div class="form-group">
297
+ <label>News Content:</label>
298
+ <textarea id="summary-news-content" rows="6" placeholder="Enter the full article text here. The AI will generate a concise summary highlighting the key points..."></textarea>
299
+ </div>
300
+ <button class="btn-primary" onclick="summarizeNews()">✨ Summarize News</button>
301
+ <div id="news-summary-result" style="margin-top: 20px;"></div>
302
+ </div>
303
+
304
  <div class="card">
305
  <h3>Analysis History</h3>
306
  <div id="sentiment-history"></div>
307
  </div>
308
  </section>
309
 
310
+ <!-- AI Analyst Tab -->
311
+ <section id="tab-ai-analyst" class="tab-content">
312
+ <div class="section-header">
313
+ <h2>🪄 AI Crypto Analyst</h2>
314
+ <p style="color: var(--text-secondary); margin-top: 10px;">Powered by OpenC/crypto-gpt-o3-mini</p>
315
+ </div>
316
+
317
+ <div class="card">
318
+ <h3>Text Generation & Analysis</h3>
319
+ <p style="color: var(--text-secondary); margin-bottom: 20px;">
320
+ Use AI to generate crypto market analysis, insights, or creative content related to crypto and DeFi.
321
+ </p>
322
+
323
+ <div class="form-group">
324
+ <label>Prompt / Question:</label>
325
+ <textarea id="ai-analyst-prompt" rows="6" placeholder="Example: Analyze the current state of Bitcoin and explain potential price movements based on market sentiment..."></textarea>
326
+ </div>
327
+
328
+ <div class="form-group">
329
+ <label>Mode:</label>
330
+ <select id="ai-analyst-mode">
331
+ <option value="analysis">Analysis</option>
332
+ <option value="generation">Generation</option>
333
+ </select>
334
+ </div>
335
+
336
+ <div class="form-group">
337
+ <label>Max Length (tokens):</label>
338
+ <select id="ai-analyst-max-length">
339
+ <option value="100">100</option>
340
+ <option value="200" selected>200</option>
341
+ <option value="300">300</option>
342
+ <option value="500">500</option>
343
+ </select>
344
+ </div>
345
+
346
+ <button class="btn-primary" onclick="runAIAnalyst()">✨ Generate Analysis</button>
347
+
348
+ <div id="ai-analyst-result" style="margin-top: 20px;"></div>
349
+ </div>
350
+
351
+ <div class="card">
352
+ <h3>💡 Example Prompts</h3>
353
+ <div style="display: grid; gap: 10px;">
354
+ <div style="padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; cursor: pointer;" onclick="setAIAnalystPrompt('Analyze the current DeFi market trends and identify key opportunities.')">
355
+ <strong>DeFi Market Analysis</strong>
356
+ <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">Analyze the current DeFi market trends and identify key opportunities.</p>
357
+ </div>
358
+ <div style="padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; cursor: pointer;" onclick="setAIAnalystPrompt('Explain how Layer 2 solutions are impacting Ethereum scalability.')">
359
+ <strong>Layer 2 Solutions</strong>
360
+ <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">Explain how Layer 2 solutions are impacting Ethereum scalability.</p>
361
+ </div>
362
+ <div style="padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; cursor: pointer;" onclick="setAIAnalystPrompt('What are the key factors affecting Bitcoin price movements?')">
363
+ <strong>Bitcoin Price Factors</strong>
364
+ <p style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">What are the key factors affecting Bitcoin price movements?</p>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </section>
369
+
370
+ <!-- Trading Assistant Tab -->
371
+ <section id="tab-trading-assistant" class="tab-content">
372
+ <div class="section-header">
373
+ <h2>📊 Trading Signal Assistant</h2>
374
+ <p style="color: var(--text-secondary); margin-top: 10px;">Powered by agarkovv/CryptoTrader-LM</p>
375
+ </div>
376
+
377
+ <div class="card">
378
+ <h3>Get Trading Decision</h3>
379
+ <p style="color: var(--text-secondary); margin-bottom: 20px;">
380
+ Get AI-powered trading signals (BUY/SELL/HOLD) based on market analysis.
381
+ </p>
382
+
383
+ <div class="form-group">
384
+ <label>Trading Symbol:</label>
385
+ <div id="trading-symbol-container">
386
+ <input type="text" id="trading-symbol" placeholder="Loading pairs..." readonly>
387
+ </div>
388
+ </div>
389
+
390
+ <div class="form-group">
391
+ <label>Market Context (Optional):</label>
392
+ <textarea id="trading-context" rows="4" placeholder="Example: Recent breakout above resistance, high volume, positive news flow..."></textarea>
393
+ </div>
394
+
395
+ <button class="btn-primary" onclick="runTradingAssistant()">🎯 Get Trading Signal</button>
396
+
397
+ <div id="trading-assistant-result" style="margin-top: 20px;"></div>
398
+ </div>
399
+
400
+ <div class="card">
401
+ <h3>⚠️ Disclaimer</h3>
402
+ <div class="alert alert-warning">
403
+ <strong>Important:</strong> This trading assistant provides AI-generated signals for informational purposes only.
404
+ It should NOT be used as the sole basis for trading decisions. Always conduct your own research,
405
+ consider multiple factors, and consult with financial professionals before making investment decisions.
406
+ Past performance does not guarantee future results.
407
+ </div>
408
+ </div>
409
+ </section>
410
+
411
  <!-- News Tab -->
412
  <section id="tab-news" class="tab-content">
413
  <div class="section-header">
 
447
  <button class="btn-primary" onclick="runDiagnostics()">▶️ Run Diagnostics</button>
448
  </div>
449
 
450
+ <div class="card">
451
+ <h3>System Health</h3>
452
+ <button class="btn-refresh" onclick="loadHealthDiagnostics()">🔄 Refresh Health</button>
453
+ <div id="health-diagnostics-result" style="margin-top: 15px;"></div>
454
+ </div>
455
+
456
  <div class="card">
457
  <h3>System Status</h3>
458
  <div id="diagnostics-status"></div>
requirements.txt CHANGED
@@ -1,5 +1,6 @@
1
  # Unified dependencies for Crypto Intelligence Hub (HuggingFace Space)
2
  # Optimized for HF deployment with minimal conflicts
 
3
 
4
  # ===== Core API Stack =====
5
  fastapi==0.115.0
 
1
  # Unified dependencies for Crypto Intelligence Hub (HuggingFace Space)
2
  # Optimized for HF deployment with minimal conflicts
3
+ # Production-ready for Hugging Face Spaces Docker environment
4
 
5
  # ===== Core API Stack =====
6
  fastapi==0.115.0
static/css/components.css CHANGED
@@ -1,203 +1,820 @@
1
- /* ============================================
2
- Components CSS - Reusable UI Components
3
- ============================================
4
-
5
- This file contains all reusable component styles:
6
- - Toast notifications
7
- - Loading spinners
8
- - Status badges (info, success, warning, danger)
9
- - Empty states
10
- - Stream items
11
- - Alerts
12
-
13
- ============================================ */
14
-
15
- /* === Toast Notification Styles === */
16
-
17
- .toast-stack {
18
- position: fixed;
19
- top: 24px;
20
- right: 24px;
21
- display: flex;
22
- flex-direction: column;
23
- gap: 12px;
24
- z-index: 2000;
25
- }
26
-
27
- .toast {
28
- min-width: 260px;
29
- background: #ffffff;
30
- border-radius: 12px;
31
- border: 1px solid var(--ui-border);
32
- padding: 14px 18px;
33
- box-shadow: var(--ui-shadow);
34
- display: flex;
35
- gap: 12px;
36
- align-items: center;
37
- animation: toast-in 220ms ease;
38
- }
39
-
40
- .toast.success { border-color: rgba(22, 163, 74, 0.4); }
41
- .toast.error { border-color: rgba(220, 38, 38, 0.4); }
42
- .toast.info { border-color: rgba(37, 99, 235, 0.4); }
43
-
44
- .toast strong {
45
- font-size: 0.95rem;
46
- color: var(--ui-text);
47
- }
48
-
49
- .toast small {
50
- color: var(--ui-text-muted);
51
- display: block;
52
- }
53
-
54
- @keyframes toast-in {
55
- from { opacity: 0; transform: translateY(-10px); }
56
- to { opacity: 1; transform: translateY(0); }
57
- }
58
-
59
- /* === Loading Spinner Styles === */
60
-
61
- .loading-indicator {
62
- display: inline-flex;
63
- align-items: center;
64
- gap: 10px;
65
- color: var(--ui-text-muted);
66
- font-size: 0.9rem;
67
- }
68
-
69
- .loading-indicator::before {
70
- content: "";
71
- width: 14px;
72
- height: 14px;
73
- border: 2px solid var(--ui-border);
74
- border-top-color: var(--ui-primary);
75
- border-radius: 50%;
76
- animation: spin 0.8s linear infinite;
77
  }
78
 
79
- @keyframes spin {
80
- to { transform: rotate(360deg); }
 
 
81
  }
82
 
83
- .fade-in {
84
- animation: fade 250ms ease;
 
85
  }
86
 
87
- @keyframes fade {
88
- from { opacity: 0; }
89
- to { opacity: 1; }
 
 
90
  }
91
 
92
- /* === Badge Styles === */
 
 
 
93
 
94
- .badge {
95
- display: inline-flex;
96
- align-items: center;
97
- gap: 6px;
98
- padding: 4px 12px;
99
- border-radius: 999px;
100
- font-size: 0.8rem;
101
- letter-spacing: 0.06em;
102
- text-transform: uppercase;
103
- border: 1px solid transparent;
104
  }
105
 
106
- .badge.info {
107
- color: var(--ui-primary);
108
- border-color: var(--ui-primary);
109
- background: rgba(37, 99, 235, 0.08);
 
 
110
  }
111
 
112
- .badge.success {
113
- color: var(--ui-success);
114
- border-color: rgba(22, 163, 74, 0.3);
115
- background: rgba(22, 163, 74, 0.08);
116
  }
117
 
118
- .badge.warning {
119
- color: var(--ui-warning);
120
- border-color: rgba(217, 119, 6, 0.3);
121
- background: rgba(217, 119, 6, 0.08);
 
122
  }
123
 
124
- .badge.danger {
125
- color: var(--ui-danger);
126
- border-color: rgba(220, 38, 38, 0.3);
127
- background: rgba(220, 38, 38, 0.08);
128
  }
129
 
130
- .badge.source-fallback {
131
- border-color: rgba(220, 38, 38, 0.3);
132
- color: var(--ui-danger);
133
- background: rgba(220, 38, 38, 0.06);
 
134
  }
135
 
136
- .badge.source-live {
137
- border-color: rgba(22, 163, 74, 0.3);
138
- color: var(--ui-success);
139
- background: rgba(22, 163, 74, 0.08);
140
  }
141
 
142
- /* === Empty State Styles === */
 
 
 
 
 
143
 
144
- .empty-state {
145
- padding: 20px;
146
- border-radius: 12px;
147
- text-align: center;
148
- border: 1px dashed var(--ui-border);
149
- color: var(--ui-text-muted);
150
- background: #fff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
- /* === Stream Item Styles === */
 
 
 
 
154
 
155
- .ws-stream {
156
- display: flex;
157
- flex-direction: column;
158
- gap: 12px;
159
- max-height: 300px;
160
- overflow-y: auto;
161
  }
162
 
163
- .stream-item {
164
- border: 1px solid var(--ui-border);
165
- border-radius: 12px;
166
- padding: 12px 14px;
167
- background: var(--ui-panel-muted);
168
  }
169
 
170
- /* === Alert Styles === */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  .alert {
173
- border-radius: 12px;
174
- padding: 12px 16px;
175
- display: flex;
176
- justify-content: space-between;
177
- align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  }
179
 
180
- .alert.info {
181
- background: rgba(37, 99, 235, 0.08);
182
- color: var(--ui-primary);
183
- border: 1px solid rgba(37, 99, 235, 0.2);
184
  }
185
 
186
- .alert.success {
187
- background: rgba(22, 163, 74, 0.08);
188
- color: var(--ui-success);
189
- border: 1px solid rgba(22, 163, 74, 0.2);
 
 
 
 
 
 
 
190
  }
191
 
192
- .alert.warning {
193
- background: rgba(217, 119, 6, 0.08);
194
- color: var(--ui-warning);
195
- border: 1px solid rgba(217, 119, 6, 0.2);
 
 
 
 
 
 
 
196
  }
197
 
198
- .alert.danger,
199
- .alert.error {
200
- background: rgba(220, 38, 38, 0.08);
201
- color: var(--ui-danger);
202
- border: 1px solid rgba(220, 38, 38, 0.2);
203
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * COMPONENTS CSS — ULTRA ENTERPRISE EDITION
4
+ * Crypto Monitor HF — Glass + Neon Component Library
5
+ * ═══════════════════════════════════════════════════════════════════
6
+ *
7
+ * All components use design-system.css tokens
8
+ * Glass morphism + Neon glows + Smooth animations
9
+ */
10
+
11
+ /* ═══════════════════════════════════════════════════════════════════
12
+ 🔘 BUTTONS
13
+ ═══════════════════════════════════════════════════════════════════ */
14
+
15
+ .btn {
16
+ display: inline-flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ gap: var(--space-2);
20
+ padding: var(--space-3) var(--space-6);
21
+ font-family: var(--font-main);
22
+ font-size: var(--fs-sm);
23
+ font-weight: var(--fw-semibold);
24
+ line-height: var(--lh-tight);
25
+ border: none;
26
+ border-radius: var(--radius-md);
27
+ cursor: pointer;
28
+ transition: all var(--transition-fast);
29
+ white-space: nowrap;
30
+ user-select: none;
31
+ min-height: 44px; /* Touch target WCAG AA */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
 
34
+ .btn:disabled {
35
+ opacity: 0.5;
36
+ cursor: not-allowed;
37
+ pointer-events: none;
38
  }
39
 
40
+ .btn:focus-visible {
41
+ outline: 2px solid var(--brand-cyan);
42
+ outline-offset: 2px;
43
  }
44
 
45
+ /* Primary Button — Gradient + Glow */
46
+ .btn-primary {
47
+ background: var(--gradient-primary);
48
+ color: var(--text-strong);
49
+ box-shadow: var(--shadow-sm), var(--glow-blue);
50
  }
51
 
52
+ .btn-primary:hover {
53
+ box-shadow: var(--shadow-md), var(--glow-blue-strong);
54
+ transform: translateY(-2px);
55
+ }
56
 
57
+ .btn-primary:active {
58
+ transform: translateY(0);
59
+ box-shadow: var(--shadow-xs), var(--glow-blue);
 
 
 
 
 
 
 
60
  }
61
 
62
+ /* Secondary Button — Glass Outline */
63
+ .btn-secondary {
64
+ background: var(--surface-glass);
65
+ color: var(--text-normal);
66
+ border: 1px solid var(--border-light);
67
+ backdrop-filter: var(--blur-md);
68
  }
69
 
70
+ .btn-secondary:hover {
71
+ background: var(--surface-glass-strong);
72
+ border-color: var(--border-medium);
73
+ transform: translateY(-1px);
74
  }
75
 
76
+ /* Success Button */
77
+ .btn-success {
78
+ background: var(--gradient-success);
79
+ color: var(--text-strong);
80
+ box-shadow: var(--shadow-sm), var(--glow-green);
81
  }
82
 
83
+ .btn-success:hover {
84
+ box-shadow: var(--shadow-md), var(--glow-green-strong);
85
+ transform: translateY(-2px);
 
86
  }
87
 
88
+ /* Danger Button */
89
+ .btn-danger {
90
+ background: var(--gradient-danger);
91
+ color: var(--text-strong);
92
+ box-shadow: var(--shadow-sm);
93
  }
94
 
95
+ .btn-danger:hover {
96
+ box-shadow: var(--shadow-md);
97
+ transform: translateY(-2px);
 
98
  }
99
 
100
+ /* Ghost Button */
101
+ .btn-ghost {
102
+ background: transparent;
103
+ color: var(--text-soft);
104
+ border: none;
105
+ }
106
 
107
+ .btn-ghost:hover {
108
+ background: var(--surface-glass);
109
+ color: var(--text-normal);
110
+ }
111
+
112
+ /* Button Sizes */
113
+ .btn-sm {
114
+ padding: var(--space-2) var(--space-4);
115
+ font-size: var(--fs-xs);
116
+ min-height: 36px;
117
+ }
118
+
119
+ .btn-lg {
120
+ padding: var(--space-4) var(--space-8);
121
+ font-size: var(--fs-base);
122
+ min-height: 52px;
123
+ }
124
+
125
+ /* Icon-only button */
126
+ .btn-icon {
127
+ padding: var(--space-3);
128
+ min-width: 44px;
129
+ min-height: 44px;
130
+ }
131
+
132
+ /* ═══════════════════════════════════════════════════════════════════
133
+ 🃏 CARDS
134
+ ═══════════════════════════════════════════════════════════════════ */
135
+
136
+ .card {
137
+ background: var(--surface-glass);
138
+ border: 1px solid var(--border-light);
139
+ border-radius: var(--radius-lg);
140
+ padding: var(--space-6);
141
+ box-shadow: var(--shadow-md);
142
+ backdrop-filter: var(--blur-lg);
143
+ transition: all var(--transition-normal);
144
+ }
145
+
146
+ .card:hover {
147
+ background: var(--surface-glass-strong);
148
+ box-shadow: var(--shadow-lg);
149
+ transform: translateY(-2px);
150
+ }
151
+
152
+ .card-header {
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: space-between;
156
+ margin-bottom: var(--space-4);
157
+ padding-bottom: var(--space-4);
158
+ border-bottom: 1px solid var(--border-subtle);
159
+ }
160
+
161
+ .card-title {
162
+ font-size: var(--fs-lg);
163
+ font-weight: var(--fw-bold);
164
+ color: var(--text-strong);
165
+ margin: 0;
166
+ display: flex;
167
+ align-items: center;
168
+ gap: var(--space-2);
169
+ }
170
+
171
+ .card-body {
172
+ color: var(--text-soft);
173
+ line-height: var(--lh-relaxed);
174
+ }
175
+
176
+ .card-footer {
177
+ margin-top: var(--space-6);
178
+ padding-top: var(--space-4);
179
+ border-top: 1px solid var(--border-subtle);
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: space-between;
183
+ }
184
+
185
+ /* Card variants */
186
+ .card-elevated {
187
+ background: var(--surface-glass-strong);
188
+ box-shadow: var(--shadow-lg);
189
+ }
190
+
191
+ .card-neon {
192
+ border-color: var(--brand-cyan);
193
+ box-shadow: var(--shadow-md), var(--glow-cyan);
194
+ }
195
+
196
+ /* ═══════════════════════════════════════════════════════════════════
197
+ 📊 STAT CARDS
198
+ ═══════════════════════════════════════════════════════════════════ */
199
+
200
+ .stat-card {
201
+ background: var(--surface-glass);
202
+ border: 1px solid var(--border-light);
203
+ border-radius: var(--radius-md);
204
+ padding: var(--space-5);
205
+ backdrop-filter: var(--blur-lg);
206
+ transition: all var(--transition-normal);
207
+ }
208
+
209
+ .stat-card:hover {
210
+ transform: translateY(-4px);
211
+ box-shadow: var(--shadow-lg), var(--glow-cyan);
212
+ border-color: var(--brand-cyan);
213
+ }
214
+
215
+ .stat-icon {
216
+ width: 48px;
217
+ height: 48px;
218
+ border-radius: var(--radius-md);
219
+ display: flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ background: var(--gradient-primary);
223
+ box-shadow: var(--glow-blue);
224
+ margin-bottom: var(--space-3);
225
+ }
226
+
227
+ .stat-value {
228
+ font-size: var(--fs-3xl);
229
+ font-weight: var(--fw-extrabold);
230
+ color: var(--text-strong);
231
+ margin-bottom: var(--space-1);
232
+ line-height: var(--lh-tight);
233
+ }
234
+
235
+ .stat-label {
236
+ font-size: var(--fs-sm);
237
+ color: var(--text-muted);
238
+ font-weight: var(--fw-medium);
239
+ text-transform: uppercase;
240
+ letter-spacing: var(--tracking-wide);
241
+ }
242
+
243
+ .stat-change {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ gap: var(--space-1);
247
+ margin-top: var(--space-2);
248
+ font-size: var(--fs-xs);
249
+ font-weight: var(--fw-semibold);
250
+ padding: var(--space-1) var(--space-2);
251
+ border-radius: var(--radius-xs);
252
+ }
253
+
254
+ .stat-change.positive {
255
+ color: var(--success);
256
+ background: rgba(34, 197, 94, 0.15);
257
+ }
258
+
259
+ .stat-change.negative {
260
+ color: var(--danger);
261
+ background: rgba(239, 68, 68, 0.15);
262
+ }
263
+
264
+ /* ═══════════════════════════════════════════════════════════════════
265
+ 🏷️ BADGES
266
+ ═══════════════════════════════════════════════════════════════════ */
267
+
268
+ .badge {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ gap: var(--space-1);
272
+ padding: var(--space-1) var(--space-3);
273
+ font-size: var(--fs-xs);
274
+ font-weight: var(--fw-semibold);
275
+ border-radius: var(--radius-full);
276
+ white-space: nowrap;
277
+ line-height: var(--lh-tight);
278
  }
279
 
280
+ .badge-primary {
281
+ background: rgba(59, 130, 246, 0.20);
282
+ color: var(--brand-blue-light);
283
+ border: 1px solid rgba(59, 130, 246, 0.40);
284
+ }
285
 
286
+ .badge-success {
287
+ background: rgba(34, 197, 94, 0.20);
288
+ color: var(--success-light);
289
+ border: 1px solid rgba(34, 197, 94, 0.40);
 
 
290
  }
291
 
292
+ .badge-warning {
293
+ background: rgba(245, 158, 11, 0.20);
294
+ color: var(--warning-light);
295
+ border: 1px solid rgba(245, 158, 11, 0.40);
 
296
  }
297
 
298
+ .badge-danger {
299
+ background: rgba(239, 68, 68, 0.20);
300
+ color: var(--danger-light);
301
+ border: 1px solid rgba(239, 68, 68, 0.40);
302
+ }
303
+
304
+ .badge-purple {
305
+ background: rgba(139, 92, 246, 0.20);
306
+ color: var(--brand-purple-light);
307
+ border: 1px solid rgba(139, 92, 246, 0.40);
308
+ }
309
+
310
+ .badge-cyan {
311
+ background: rgba(6, 182, 212, 0.20);
312
+ color: var(--brand-cyan-light);
313
+ border: 1px solid rgba(6, 182, 212, 0.40);
314
+ }
315
+
316
+ /* ═══════════════════════════════════════════════════════════════════
317
+ ⚠️ ALERTS
318
+ ═══════════════════════════════════════════════════════════════════ */
319
 
320
  .alert {
321
+ padding: var(--space-4) var(--space-5);
322
+ border-radius: var(--radius-md);
323
+ border-left: 4px solid;
324
+ backdrop-filter: var(--blur-md);
325
+ display: flex;
326
+ align-items: start;
327
+ gap: var(--space-3);
328
+ margin-bottom: var(--space-4);
329
+ }
330
+
331
+ .alert-info {
332
+ background: rgba(14, 165, 233, 0.15);
333
+ border-left-color: var(--info);
334
+ color: var(--info-light);
335
+ }
336
+
337
+ .alert-success {
338
+ background: rgba(34, 197, 94, 0.15);
339
+ border-left-color: var(--success);
340
+ color: var(--success-light);
341
+ }
342
+
343
+ .alert-warning {
344
+ background: rgba(245, 158, 11, 0.15);
345
+ border-left-color: var(--warning);
346
+ color: var(--warning-light);
347
+ }
348
+
349
+ .alert-error {
350
+ background: rgba(239, 68, 68, 0.15);
351
+ border-left-color: var(--danger);
352
+ color: var(--danger-light);
353
+ }
354
+
355
+ .alert-icon {
356
+ flex-shrink: 0;
357
+ width: 20px;
358
+ height: 20px;
359
+ }
360
+
361
+ .alert-content {
362
+ flex: 1;
363
+ }
364
+
365
+ .alert-title {
366
+ font-weight: var(--fw-semibold);
367
+ margin-bottom: var(--space-1);
368
+ }
369
+
370
+ .alert-description {
371
+ font-size: var(--fs-sm);
372
+ opacity: 0.9;
373
+ }
374
+
375
+ /* ═══════════════════════════════════════════════════════════════════
376
+ 📋 TABLES
377
+ ═══════════════════════════════════════════════════════════════════ */
378
+
379
+ .table-container {
380
+ overflow-x: auto;
381
+ border-radius: var(--radius-lg);
382
+ background: var(--surface-glass);
383
+ border: 1px solid var(--border-light);
384
+ backdrop-filter: var(--blur-lg);
385
+ }
386
+
387
+ .table {
388
+ width: 100%;
389
+ border-collapse: collapse;
390
+ }
391
+
392
+ .table thead {
393
+ background: rgba(255, 255, 255, 0.14);
394
+ position: sticky;
395
+ top: 0;
396
+ z-index: var(--z-sticky);
397
+ }
398
+
399
+ .table th {
400
+ padding: var(--space-4) var(--space-5);
401
+ text-align: left;
402
+ font-size: var(--fs-xs);
403
+ font-weight: var(--fw-bold);
404
+ color: var(--text-soft);
405
+ text-transform: uppercase;
406
+ letter-spacing: var(--tracking-wider);
407
+ border-bottom: 2px solid var(--border-medium);
408
+ }
409
+
410
+ .table td {
411
+ padding: var(--space-4) var(--space-5);
412
+ border-bottom: 1px solid var(--border-subtle);
413
+ color: var(--text-normal);
414
+ }
415
+
416
+ .table tbody tr {
417
+ transition: all var(--transition-fast);
418
+ }
419
+
420
+ .table tbody tr:hover {
421
+ background: rgba(255, 255, 255, 0.10);
422
+ box-shadow: inset 0 0 0 1px var(--brand-cyan), inset 0 0 12px rgba(6, 182, 212, 0.25);
423
+ }
424
+
425
+ .table tbody tr:last-child td {
426
+ border-bottom: none;
427
+ }
428
+
429
+ /* ═══════════════════════════════════════════════════════════════════
430
+ 🔴 STATUS DOTS
431
+ ═══════════════════════════════════════════════════════════════════ */
432
+
433
+ .status-dot {
434
+ display: inline-block;
435
+ width: 10px;
436
+ height: 10px;
437
+ border-radius: 50%;
438
+ margin-right: var(--space-2);
439
+ }
440
+
441
+ .status-online {
442
+ background: var(--success);
443
+ box-shadow: 0 0 12px var(--success), 0 0 24px rgba(34, 197, 94, 0.40);
444
+ animation: pulse-green 2s infinite;
445
+ }
446
+
447
+ .status-offline {
448
+ background: var(--danger);
449
+ box-shadow: 0 0 12px var(--danger);
450
+ }
451
+
452
+ .status-degraded {
453
+ background: var(--warning);
454
+ box-shadow: 0 0 12px var(--warning);
455
+ animation: pulse-yellow 2s infinite;
456
+ }
457
+
458
+ @keyframes pulse-green {
459
+ 0%, 100% {
460
+ box-shadow: 0 0 12px var(--success), 0 0 24px rgba(34, 197, 94, 0.40);
461
+ }
462
+ 50% {
463
+ box-shadow: 0 0 16px var(--success), 0 0 32px rgba(34, 197, 94, 0.60);
464
+ }
465
+ }
466
+
467
+ @keyframes pulse-yellow {
468
+ 0%, 100% {
469
+ box-shadow: 0 0 12px var(--warning), 0 0 24px rgba(245, 158, 11, 0.40);
470
+ }
471
+ 50% {
472
+ box-shadow: 0 0 16px var(--warning), 0 0 32px rgba(245, 158, 11, 0.60);
473
+ }
474
+ }
475
+
476
+ /* ═══════════════════════════════════════════════════════════════════
477
+ ⏳ LOADING STATES
478
+ ═══════════════════════════════════════════════════════════════════ */
479
+
480
+ .loading {
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: center;
484
+ padding: var(--space-12);
485
+ }
486
+
487
+ .spinner {
488
+ width: 40px;
489
+ height: 40px;
490
+ border: 3px solid var(--border-light);
491
+ border-top-color: var(--brand-cyan);
492
+ border-radius: 50%;
493
+ animation: spin 0.8s linear infinite;
494
+ box-shadow: var(--glow-cyan);
495
+ }
496
+
497
+ @keyframes spin {
498
+ to {
499
+ transform: rotate(360deg);
500
+ }
501
+ }
502
+
503
+ .skeleton {
504
+ background: linear-gradient(
505
+ 90deg,
506
+ rgba(255, 255, 255, 0.08) 0%,
507
+ rgba(255, 255, 255, 0.14) 50%,
508
+ rgba(255, 255, 255, 0.08) 100%
509
+ );
510
+ background-size: 200% 100%;
511
+ animation: skeleton-loading 1.5s ease-in-out infinite;
512
+ border-radius: var(--radius-md);
513
+ }
514
+
515
+ @keyframes skeleton-loading {
516
+ 0% {
517
+ background-position: 200% 0;
518
+ }
519
+ 100% {
520
+ background-position: -200% 0;
521
+ }
522
+ }
523
+
524
+ /* ═══════════════════════════════════════════════════════════════════
525
+ 📝 FORMS & INPUTS
526
+ ═══════════════════════════════════════════════════════════════════ */
527
+
528
+ .form-group {
529
+ margin-bottom: var(--space-5);
530
+ }
531
+
532
+ .form-label {
533
+ display: block;
534
+ font-size: var(--fs-sm);
535
+ font-weight: var(--fw-semibold);
536
+ margin-bottom: var(--space-2);
537
+ color: var(--text-normal);
538
+ }
539
+
540
+ .form-input,
541
+ .form-select,
542
+ .form-textarea {
543
+ width: 100%;
544
+ padding: var(--space-3) var(--space-4);
545
+ font-family: var(--font-main);
546
+ font-size: var(--fs-base);
547
+ color: var(--text-strong);
548
+ background: var(--input-bg);
549
+ border: 1px solid var(--border-light);
550
+ border-radius: var(--radius-sm);
551
+ backdrop-filter: var(--blur-md);
552
+ transition: all var(--transition-fast);
553
+ }
554
+
555
+ .form-input:focus,
556
+ .form-select:focus,
557
+ .form-textarea:focus {
558
+ outline: none;
559
+ border-color: var(--brand-cyan);
560
+ box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.30), var(--glow-cyan);
561
+ background: rgba(15, 23, 42, 0.80);
562
+ }
563
+
564
+ .form-input::placeholder {
565
+ color: var(--text-faint);
566
+ }
567
+
568
+ .form-input:disabled,
569
+ .form-select:disabled,
570
+ .form-textarea:disabled {
571
+ background: var(--surface-glass);
572
+ cursor: not-allowed;
573
+ opacity: 0.6;
574
+ }
575
+
576
+ .form-error {
577
+ color: var(--danger);
578
+ font-size: var(--fs-xs);
579
+ margin-top: var(--space-1);
580
+ display: flex;
581
+ align-items: center;
582
+ gap: var(--space-1);
583
+ }
584
+
585
+ .form-help {
586
+ color: var(--text-muted);
587
+ font-size: var(--fs-xs);
588
+ margin-top: var(--space-1);
589
+ }
590
+
591
+ /* ═══════════════════════════════════════════════════════════════════
592
+ 🔘 TOGGLE SWITCH
593
+ ═══════════════════════════════════════════════════════════════════ */
594
+
595
+ .toggle-switch {
596
+ position: relative;
597
+ display: inline-block;
598
+ width: 52px;
599
+ height: 28px;
600
  }
601
 
602
+ .toggle-switch input {
603
+ opacity: 0;
604
+ width: 0;
605
+ height: 0;
606
  }
607
 
608
+ .toggle-slider {
609
+ position: absolute;
610
+ cursor: pointer;
611
+ top: 0;
612
+ left: 0;
613
+ right: 0;
614
+ bottom: 0;
615
+ background: var(--surface-glass);
616
+ border: 1px solid var(--border-light);
617
+ transition: var(--transition-normal);
618
+ border-radius: var(--radius-full);
619
  }
620
 
621
+ .toggle-slider:before {
622
+ position: absolute;
623
+ content: "";
624
+ height: 20px;
625
+ width: 20px;
626
+ left: 4px;
627
+ bottom: 3px;
628
+ background: var(--text-strong);
629
+ transition: var(--transition-normal);
630
+ border-radius: 50%;
631
+ box-shadow: var(--shadow-sm);
632
  }
633
 
634
+ .toggle-switch input:checked + .toggle-slider {
635
+ background: var(--gradient-primary);
636
+ box-shadow: var(--glow-blue);
637
+ border-color: transparent;
 
638
  }
639
+
640
+ .toggle-switch input:checked + .toggle-slider:before {
641
+ transform: translateX(24px);
642
+ }
643
+
644
+ .toggle-switch input:focus-visible + .toggle-slider {
645
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.30);
646
+ }
647
+
648
+ /* ═══════════════════════════════════════════════════════════════════
649
+ 🔳 MODAL
650
+ ═══════════════════════════════════════════════════════════════════ */
651
+
652
+ .modal-overlay {
653
+ position: fixed;
654
+ top: 0;
655
+ left: 0;
656
+ right: 0;
657
+ bottom: 0;
658
+ background: var(--modal-backdrop);
659
+ backdrop-filter: var(--blur-xl);
660
+ display: flex;
661
+ align-items: center;
662
+ justify-content: center;
663
+ z-index: var(--z-modal);
664
+ padding: var(--space-6);
665
+ animation: modal-fade-in 0.2s ease-out;
666
+ }
667
+
668
+ @keyframes modal-fade-in {
669
+ from {
670
+ opacity: 0;
671
+ }
672
+ to {
673
+ opacity: 1;
674
+ }
675
+ }
676
+
677
+ .modal {
678
+ background: var(--surface-glass-stronger);
679
+ border: 1px solid var(--border-medium);
680
+ border-radius: var(--radius-xl);
681
+ box-shadow: var(--shadow-2xl);
682
+ backdrop-filter: var(--blur-lg);
683
+ max-width: 600px;
684
+ width: 100%;
685
+ max-height: 90vh;
686
+ overflow-y: auto;
687
+ animation: modal-scale-in 0.25s var(--ease-spring);
688
+ }
689
+
690
+ @keyframes modal-scale-in {
691
+ from {
692
+ transform: scale(0.95);
693
+ opacity: 0;
694
+ }
695
+ to {
696
+ transform: scale(1);
697
+ opacity: 1;
698
+ }
699
+ }
700
+
701
+ .modal-header {
702
+ padding: var(--space-6) var(--space-7);
703
+ border-bottom: 1px solid var(--border-subtle);
704
+ display: flex;
705
+ align-items: center;
706
+ justify-content: space-between;
707
+ }
708
+
709
+ .modal-title {
710
+ font-size: var(--fs-xl);
711
+ font-weight: var(--fw-bold);
712
+ color: var(--text-strong);
713
+ margin: 0;
714
+ }
715
+
716
+ .modal-close {
717
+ width: 36px;
718
+ height: 36px;
719
+ border-radius: var(--radius-sm);
720
+ display: flex;
721
+ align-items: center;
722
+ justify-content: center;
723
+ color: var(--text-soft);
724
+ background: transparent;
725
+ border: none;
726
+ cursor: pointer;
727
+ transition: var(--transition-fast);
728
+ }
729
+
730
+ .modal-close:hover {
731
+ background: var(--surface-glass);
732
+ color: var(--text-strong);
733
+ }
734
+
735
+ .modal-body {
736
+ padding: var(--space-7);
737
+ color: var(--text-normal);
738
+ }
739
+
740
+ .modal-footer {
741
+ padding: var(--space-6) var(--space-7);
742
+ border-top: 1px solid var(--border-subtle);
743
+ display: flex;
744
+ align-items: center;
745
+ justify-content: flex-end;
746
+ gap: var(--space-3);
747
+ }
748
+
749
+ /* ═══════════════════════════════════════════════════════════════════
750
+ 📈 CHARTS & VISUALIZATION
751
+ ═══════════════════════════════════════════════════════════════════ */
752
+
753
+ .chart-container {
754
+ position: relative;
755
+ width: 100%;
756
+ max-width: 100%;
757
+ padding: var(--space-4);
758
+ background: var(--surface-glass);
759
+ border: 1px solid var(--border-light);
760
+ border-radius: var(--radius-md);
761
+ backdrop-filter: var(--blur-md);
762
+ }
763
+
764
+ .chart-container canvas {
765
+ width: 100% !important;
766
+ height: auto !important;
767
+ max-height: 400px;
768
+ }
769
+
770
+ /* ═══════════════════════════════════════════════════════════════════
771
+ 📐 GRID LAYOUTS
772
+ ═══════════════════════════════════════════════════════════════════ */
773
+
774
+ .stats-grid {
775
+ display: grid;
776
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
777
+ gap: var(--space-5);
778
+ margin-bottom: var(--space-8);
779
+ }
780
+
781
+ .cards-grid {
782
+ display: grid;
783
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
784
+ gap: var(--space-6);
785
+ }
786
+
787
+ /* ═══════════════════════════════════════════════════════════════════
788
+ 🎯 EMPTY STATE
789
+ ═══════════════════════════════════════════════════════════════════ */
790
+
791
+ .empty-state {
792
+ text-align: center;
793
+ padding: var(--space-12);
794
+ color: var(--text-muted);
795
+ }
796
+
797
+ .empty-state-icon {
798
+ font-size: 64px;
799
+ margin-bottom: var(--space-4);
800
+ opacity: 0.4;
801
+ }
802
+
803
+ .empty-state-title {
804
+ font-size: var(--fs-lg);
805
+ font-weight: var(--fw-semibold);
806
+ margin-bottom: var(--space-2);
807
+ color: var(--text-normal);
808
+ }
809
+
810
+ .empty-state-description {
811
+ font-size: var(--fs-sm);
812
+ margin-bottom: var(--space-6);
813
+ max-width: 400px;
814
+ margin-left: auto;
815
+ margin-right: auto;
816
+ }
817
+
818
+ /* ═══════════════════════════════════════════════════════════════════
819
+ 🏗️ END OF COMPONENTS
820
+ ═══════════════════════════════════════════════════════════════════ */
static/css/design-system.css CHANGED
@@ -148,7 +148,7 @@
148
 
149
  :root {
150
  /* ━━━ FONT FAMILIES ━━━ */
151
- --font-main: "Inter", "Poppins", "Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
152
  --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, monospace;
153
 
154
  /* ━━━ FONT SIZES ━━━ */
 
148
 
149
  :root {
150
  /* ━━━ FONT FAMILIES ━━━ */
151
+ --font-main: "Inter", "Rubik", "Vazirmatn", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
152
  --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, monospace;
153
 
154
  /* ━━━ FONT SIZES ━━━ */
static/css/design-tokens.css CHANGED
@@ -1,129 +1,96 @@
1
  /**
2
  * ============================================
3
- * ENHANCED DESIGN TOKENS - Admin UI Modernization
4
- * Crypto Intelligence Hub
5
  * ============================================
6
  *
7
- * Comprehensive design system with:
8
- * - Color palette (dark/light themes)
9
- * - Gradients (linear, radial, glass effects)
10
- * - Typography scale (fonts, sizes, weights, spacing)
11
- * - Spacing system (consistent rhythm)
12
  * - Border radius tokens
13
- * - Multi-layered shadow system
14
- * - Blur effect variables
15
- * - Transition and easing functions
16
- * - Z-index elevation levels
17
- * - Layout constants
18
  */
19
 
20
  :root {
21
- /* ===== COLOR PALETTE - DARK THEME (DEFAULT) ===== */
22
 
23
- /* Primary Brand Colors */
24
- --color-primary: #6366f1;
25
- --color-primary-light: #818cf8;
26
- --color-primary-dark: #4f46e5;
27
- --color-primary-darker: #4338ca;
28
-
29
- /* Accent Colors */
30
- --color-accent: #ec4899;
31
- --color-accent-light: #f472b6;
32
- --color-accent-dark: #db2777;
33
-
34
- /* Semantic Colors */
35
- --color-success: #10b981;
36
- --color-success-light: #34d399;
37
- --color-success-dark: #059669;
38
-
39
- --color-warning: #f59e0b;
40
- --color-warning-light: #fbbf24;
41
- --color-warning-dark: #d97706;
42
-
43
- --color-error: #ef4444;
44
- --color-error-light: #f87171;
45
- --color-error-dark: #dc2626;
46
-
47
- --color-info: #3b82f6;
48
- --color-info-light: #60a5fa;
49
- --color-info-dark: #2563eb;
50
-
51
- /* Extended Palette */
52
- --color-purple: #8b5cf6;
53
- --color-purple-light: #a78bfa;
54
- --color-purple-dark: #7c3aed;
55
-
56
- --color-cyan: #06b6d4;
57
- --color-cyan-light: #22d3ee;
58
- --color-cyan-dark: #0891b2;
59
-
60
- --color-orange: #f97316;
61
- --color-orange-light: #fb923c;
62
- --color-orange-dark: #ea580c;
63
-
64
- /* Background Colors - Dark Theme */
65
- --bg-primary: #0f172a;
66
- --bg-secondary: #1e293b;
67
- --bg-tertiary: #334155;
68
- --bg-elevated: #1e293b;
69
- --bg-overlay: rgba(0, 0, 0, 0.75);
70
 
71
  /* Glassmorphism Backgrounds */
72
- --glass-bg: rgba(255, 255, 255, 0.05);
73
- --glass-bg-light: rgba(255, 255, 255, 0.08);
74
- --glass-bg-strong: rgba(255, 255, 255, 0.12);
75
- --glass-border: rgba(255, 255, 255, 0.1);
76
- --glass-border-strong: rgba(255, 255, 255, 0.2);
77
 
78
  /* Text Colors */
79
- --text-primary: #f1f5f9;
80
- --text-secondary: #cbd5e1;
81
- --text-tertiary: #94a3b8;
82
- --text-muted: #64748b;
83
- --text-disabled: #475569;
84
- --text-inverse: #0f172a;
85
 
86
- /* Border Colors */
87
- --border-color: rgba(255, 255, 255, 0.1);
88
- --border-color-light: rgba(255, 255, 255, 0.05);
89
- --border-color-strong: rgba(255, 255, 255, 0.2);
90
- --border-focus: var(--color-primary);
91
 
92
- /* ===== GRADIENTS ===== */
 
 
93
 
94
- /* Primary Gradients */
95
- --gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
96
- --gradient-accent: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
97
- --gradient-success: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
98
- --gradient-warning: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
99
- --gradient-error: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
100
 
101
- /* Glass Gradients */
102
- --gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
103
- --gradient-glass-strong: linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.08) 100%);
104
 
105
- /* Overlay Gradients */
106
- --gradient-overlay: linear-gradient(180deg, rgba(15,23,42,0) 0%, rgba(15,23,42,0.8) 100%);
107
- --gradient-overlay-radial: radial-gradient(circle at center, rgba(15,23,42,0) 0%, rgba(15,23,42,0.9) 100%);
108
 
109
- /* Radial Gradients for Backgrounds */
110
- --gradient-radial-blue: radial-gradient(circle at 20% 30%, rgba(99,102,241,0.15) 0%, transparent 50%);
111
- --gradient-radial-purple: radial-gradient(circle at 80% 70%, rgba(139,92,246,0.15) 0%, transparent 50%);
112
- --gradient-radial-pink: radial-gradient(circle at 50% 50%, rgba(236,72,153,0.1) 0%, transparent 40%);
113
- --gradient-radial-green: radial-gradient(circle at 60% 40%, rgba(16,185,129,0.1) 0%, transparent 40%);
114
 
115
- /* Multi-color Gradients */
116
- --gradient-rainbow: linear-gradient(135deg, #667eea 0%, #764ba2 33%, #f093fb 66%, #4facfe 100%);
117
- --gradient-sunset: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
118
- --gradient-ocean: linear-gradient(135deg, #2e3192 0%, #1bffff 100%);
119
 
120
- /* ===== TYPOGRAPHY ===== */
 
 
 
 
121
 
122
- /* Font Families */
123
- --font-family-primary: 'Inter', 'Manrope', 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
124
- --font-family-secondary: 'Manrope', 'Inter', sans-serif;
125
- --font-family-display: 'DM Sans', 'Inter', sans-serif;
126
- --font-family-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', 'Consolas', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  /* Font Sizes */
129
  --font-size-xs: 0.75rem; /* 12px */
@@ -135,7 +102,6 @@
135
  --font-size-2xl: 1.875rem; /* 30px */
136
  --font-size-3xl: 2.25rem; /* 36px */
137
  --font-size-4xl: 3rem; /* 48px */
138
- --font-size-5xl: 3.75rem; /* 60px */
139
 
140
  /* Font Weights */
141
  --font-weight-light: 300;
@@ -148,125 +114,74 @@
148
 
149
  /* Line Heights */
150
  --line-height-tight: 1.25;
151
- --line-height-snug: 1.375;
152
  --line-height-normal: 1.5;
153
- --line-height-relaxed: 1.625;
154
- --line-height-loose: 1.75;
155
- --line-height-loose-2: 2;
156
-
157
- /* Letter Spacing */
158
- --letter-spacing-tighter: -0.05em;
159
- --letter-spacing-tight: -0.025em;
160
- --letter-spacing-normal: 0;
161
- --letter-spacing-wide: 0.025em;
162
- --letter-spacing-wider: 0.05em;
163
- --letter-spacing-widest: 0.1em;
164
 
165
  /* ===== SPACING SCALE ===== */
166
- --space-0: 0;
167
- --space-1: 0.25rem; /* 4px */
168
- --space-2: 0.5rem; /* 8px */
169
- --space-3: 0.75rem; /* 12px */
170
- --space-4: 1rem; /* 16px */
171
- --space-5: 1.25rem; /* 20px */
172
- --space-6: 1.5rem; /* 24px */
173
- --space-7: 1.75rem; /* 28px */
174
- --space-8: 2rem; /* 32px */
175
- --space-10: 2.5rem; /* 40px */
176
- --space-12: 3rem; /* 48px */
177
- --space-16: 4rem; /* 64px */
178
- --space-20: 5rem; /* 80px */
179
- --space-24: 6rem; /* 96px */
180
- --space-32: 8rem; /* 128px */
181
 
182
  /* Semantic Spacing */
183
- --spacing-xs: var(--space-1);
184
- --spacing-sm: var(--space-2);
185
- --spacing-md: var(--space-4);
186
- --spacing-lg: var(--space-6);
187
- --spacing-xl: var(--space-8);
188
- --spacing-2xl: var(--space-12);
189
- --spacing-3xl: var(--space-16);
190
 
191
  /* ===== BORDER RADIUS ===== */
192
  --radius-none: 0;
193
- --radius-xs: 0.25rem; /* 4px */
194
- --radius-sm: 0.375rem; /* 6px */
195
  --radius-base: 0.5rem; /* 8px */
196
  --radius-md: 0.75rem; /* 12px */
197
  --radius-lg: 1rem; /* 16px */
198
- --radius-xl: 1.5rem; /* 24px */
199
- --radius-2xl: 2rem; /* 32px */
200
- --radius-3xl: 3rem; /* 48px */
201
  --radius-full: 9999px;
202
 
203
- /* ===== MULTI-LAYERED SHADOW SYSTEM ===== */
204
-
205
- /* Base Shadows - Dark Theme */
206
- --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
207
- --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2);
208
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
209
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
210
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
211
- --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
212
-
213
- /* Colored Glow Shadows */
214
- --shadow-glow: 0 0 20px rgba(99,102,241,0.3);
215
- --shadow-glow-accent: 0 0 20px rgba(236,72,153,0.3);
216
- --shadow-glow-success: 0 0 20px rgba(16,185,129,0.3);
217
- --shadow-glow-warning: 0 0 20px rgba(245,158,11,0.3);
218
- --shadow-glow-error: 0 0 20px rgba(239,68,68,0.3);
219
-
220
- /* Multi-layered Colored Shadows */
221
- --shadow-blue: 0 10px 30px -5px rgba(59, 130, 246, 0.4), 0 0 15px rgba(59, 130, 246, 0.2);
222
- --shadow-purple: 0 10px 30px -5px rgba(139, 92, 246, 0.4), 0 0 15px rgba(139, 92, 246, 0.2);
223
- --shadow-pink: 0 10px 30px -5px rgba(236, 72, 153, 0.4), 0 0 15px rgba(236, 72, 153, 0.2);
224
- --shadow-green: 0 10px 30px -5px rgba(16, 185, 129, 0.4), 0 0 15px rgba(16, 185, 129, 0.2);
225
- --shadow-cyan: 0 10px 30px -5px rgba(6, 182, 212, 0.4), 0 0 15px rgba(6, 182, 212, 0.2);
226
 
227
  /* Inner Shadows */
228
- --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.3);
229
- --shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.4);
230
 
231
- /* ===== BLUR EFFECT VARIABLES ===== */
232
  --blur-none: 0;
233
- --blur-xs: 2px;
234
  --blur-sm: 4px;
235
  --blur-base: 8px;
236
  --blur-md: 12px;
237
  --blur-lg: 16px;
238
- --blur-xl: 24px;
239
  --blur-2xl: 40px;
240
  --blur-3xl: 64px;
241
 
242
- /* ===== TRANSITION AND EASING FUNCTIONS ===== */
243
-
244
- /* Duration */
245
- --transition-instant: 0ms;
246
- --transition-fast: 150ms;
247
- --transition-base: 250ms;
248
- --transition-slow: 350ms;
249
- --transition-slower: 500ms;
250
- --transition-slowest: 700ms;
251
-
252
- /* Easing Functions */
253
- --ease-linear: linear;
254
- --ease-in: cubic-bezier(0.4, 0, 1, 1);
255
- --ease-out: cubic-bezier(0, 0, 0.2, 1);
256
- --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
257
- --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
258
- --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
259
- --ease-smooth: cubic-bezier(0.25, 0.1, 0.25, 1);
260
-
261
- /* Combined Transitions */
262
- --transition-all-fast: all var(--transition-fast) var(--ease-out);
263
- --transition-all-base: all var(--transition-base) var(--ease-in-out);
264
- --transition-all-slow: all var(--transition-slow) var(--ease-in-out);
265
- --transition-transform: transform var(--transition-base) var(--ease-out);
266
- --transition-opacity: opacity var(--transition-base) var(--ease-out);
267
- --transition-colors: color var(--transition-base) var(--ease-out), background-color var(--transition-base) var(--ease-out), border-color var(--transition-base) var(--ease-out);
268
-
269
- /* ===== Z-INDEX ELEVATION LEVELS ===== */
270
  --z-base: 0;
271
  --z-dropdown: 1000;
272
  --z-sticky: 1020;
@@ -276,13 +191,27 @@
276
  --z-popover: 1060;
277
  --z-tooltip: 1070;
278
  --z-notification: 1080;
279
- --z-max: 9999;
280
 
281
- /* ===== LAYOUT CONSTANTS ===== */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  --header-height: 72px;
283
  --sidebar-width: 280px;
284
  --sidebar-collapsed-width: 80px;
285
  --mobile-nav-height: 64px;
 
286
  --container-max-width: 1920px;
287
  --content-max-width: 1440px;
288
 
@@ -294,80 +223,52 @@
294
  --breakpoint-xl: 1024px;
295
  --breakpoint-2xl: 1280px;
296
  --breakpoint-3xl: 1440px;
297
- --breakpoint-4xl: 1920px;
298
  }
299
 
300
- /* ===== LIGHT THEME OVERRIDES ===== */
301
  [data-theme="light"] {
302
- /* Background Colors */
303
- --bg-primary: #ffffff;
304
- --bg-secondary: #f9fafb;
305
- --bg-tertiary: #f3f4f6;
306
- --bg-elevated: #ffffff;
307
- --bg-overlay: rgba(255, 255, 255, 0.9);
308
-
309
- /* Glassmorphism Backgrounds */
310
- --glass-bg: rgba(255, 255, 255, 0.7);
311
- --glass-bg-light: rgba(255, 255, 255, 0.5);
312
- --glass-bg-strong: rgba(255, 255, 255, 0.85);
313
- --glass-border: rgba(0, 0, 0, 0.1);
314
- --glass-border-strong: rgba(0, 0, 0, 0.2);
315
-
316
- /* Text Colors */
317
- --text-primary: #111827;
318
- --text-secondary: #6b7280;
319
- --text-tertiary: #9ca3af;
320
- --text-muted: #d1d5db;
321
- --text-disabled: #e5e7eb;
322
- --text-inverse: #ffffff;
323
-
324
- /* Border Colors */
325
- --border-color: rgba(0, 0, 0, 0.1);
326
- --border-color-light: rgba(0, 0, 0, 0.05);
327
- --border-color-strong: rgba(0, 0, 0, 0.2);
328
-
329
- /* Glass Gradients */
330
- --gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.6) 100%);
331
- --gradient-glass-strong: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%);
332
-
333
- /* Overlay Gradients */
334
- --gradient-overlay: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.8) 100%);
335
-
336
- /* Shadows - Lighter for Light Theme */
337
- --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
338
- --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
339
- --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
340
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.08);
341
- --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.12), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
342
- --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
343
-
344
- /* Inner Shadows */
345
- --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
346
- --shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.1);
347
  }
348
 
349
  /* ===== UTILITY CLASSES ===== */
350
 
351
  /* Glassmorphism Effects */
352
  .glass-effect {
353
- background: var(--glass-bg);
354
- backdrop-filter: blur(var(--blur-lg));
355
- -webkit-backdrop-filter: blur(var(--blur-lg));
356
- border: 1px solid var(--glass-border);
357
  }
358
 
359
  .glass-effect-light {
360
- background: var(--glass-bg-light);
361
- backdrop-filter: blur(var(--blur-md));
362
- -webkit-backdrop-filter: blur(var(--blur-md));
363
- border: 1px solid var(--glass-border);
364
- }
365
-
366
- .glass-effect-strong {
367
- background: var(--glass-bg-strong);
368
- backdrop-filter: blur(var(--blur-xl));
369
- -webkit-backdrop-filter: blur(var(--blur-xl));
370
- border: 1px solid var(--glass-border-strong);
371
  }
372
 
373
  /* Gradient Backgrounds */
@@ -375,12 +276,8 @@
375
  background: var(--gradient-primary);
376
  }
377
 
378
- .bg-gradient-accent {
379
- background: var(--gradient-accent);
380
- }
381
-
382
- .bg-gradient-success {
383
- background: var(--gradient-success);
384
  }
385
 
386
  /* Text Gradients */
@@ -391,13 +288,6 @@
391
  -webkit-text-fill-color: transparent;
392
  }
393
 
394
- .text-gradient-accent {
395
- background: var(--gradient-accent);
396
- -webkit-background-clip: text;
397
- background-clip: text;
398
- -webkit-text-fill-color: transparent;
399
- }
400
-
401
  /* Shadow Utilities */
402
  .shadow-glow-blue {
403
  box-shadow: var(--shadow-blue);
@@ -417,25 +307,13 @@
417
 
418
  /* Animation Utilities */
419
  .transition-fast {
420
- transition: var(--transition-all-fast);
421
  }
422
 
423
  .transition-base {
424
- transition: var(--transition-all-base);
425
  }
426
 
427
  .transition-slow {
428
- transition: var(--transition-all-slow);
429
- }
430
-
431
- /* Accessibility: Respect reduced motion preference */
432
- @media (prefers-reduced-motion: reduce) {
433
- *,
434
- *::before,
435
- *::after {
436
- animation-duration: 0.01ms !important;
437
- animation-iteration-count: 1 !important;
438
- transition-duration: 0.01ms !important;
439
- scroll-behavior: auto !important;
440
- }
441
  }
 
1
  /**
2
  * ============================================
3
+ * DESIGN TOKENS - Enterprise Edition
4
+ * Crypto Monitor Ultimate
5
  * ============================================
6
  *
7
+ * Complete design system with:
8
+ * - Color palette (light/dark)
9
+ * - Typography scale
10
+ * - Spacing system
 
11
  * - Border radius tokens
12
+ * - Shadow system
13
+ * - Blur tokens
14
+ * - Elevation levels
15
+ * - Animation timings
 
16
  */
17
 
18
  :root {
19
+ /* ===== COLOR PALETTE ===== */
20
 
21
+ /* Base Colors - Dark Mode */
22
+ --color-bg-primary: #0a0e1a;
23
+ --color-bg-secondary: #111827;
24
+ --color-bg-tertiary: #1f2937;
25
+ --color-bg-elevated: #1f2937;
26
+ --color-bg-overlay: rgba(0, 0, 0, 0.75);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
  /* Glassmorphism Backgrounds */
29
+ --color-glass-bg: rgba(17, 24, 39, 0.7);
30
+ --color-glass-bg-light: rgba(31, 41, 55, 0.5);
31
+ --color-glass-border: rgba(255, 255, 255, 0.1);
 
 
32
 
33
  /* Text Colors */
34
+ --color-text-primary: #f9fafb;
35
+ --color-text-secondary: #9ca3af;
36
+ --color-text-tertiary: #6b7280;
37
+ --color-text-disabled: #4b5563;
38
+ --color-text-inverse: #0a0e1a;
 
39
 
40
+ /* Accent Colors - Neon Palette */
41
+ --color-accent-blue: #3b82f6;
42
+ --color-accent-blue-dark: #2563eb;
43
+ --color-accent-blue-light: #60a5fa;
 
44
 
45
+ --color-accent-purple: #8b5cf6;
46
+ --color-accent-purple-dark: #7c3aed;
47
+ --color-accent-purple-light: #a78bfa;
48
 
49
+ --color-accent-pink: #ec4899;
50
+ --color-accent-pink-dark: #db2777;
51
+ --color-accent-pink-light: #f472b6;
 
 
 
52
 
53
+ --color-accent-green: #10b981;
54
+ --color-accent-green-dark: #059669;
55
+ --color-accent-green-light: #34d399;
56
 
57
+ --color-accent-yellow: #f59e0b;
58
+ --color-accent-yellow-dark: #d97706;
59
+ --color-accent-yellow-light: #fbbf24;
60
 
61
+ --color-accent-red: #ef4444;
62
+ --color-accent-red-dark: #dc2626;
63
+ --color-accent-red-light: #f87171;
 
 
64
 
65
+ --color-accent-cyan: #06b6d4;
66
+ --color-accent-cyan-dark: #0891b2;
67
+ --color-accent-cyan-light: #22d3ee;
 
68
 
69
+ /* Semantic Colors */
70
+ --color-success: var(--color-accent-green);
71
+ --color-error: var(--color-accent-red);
72
+ --color-warning: var(--color-accent-yellow);
73
+ --color-info: var(--color-accent-blue);
74
 
75
+ /* Border Colors */
76
+ --color-border-primary: rgba(255, 255, 255, 0.1);
77
+ --color-border-secondary: rgba(255, 255, 255, 0.05);
78
+ --color-border-focus: var(--color-accent-blue);
79
+
80
+ /* ===== GRADIENTS ===== */
81
+ --gradient-primary: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
82
+ --gradient-secondary: linear-gradient(135deg, #10b981 0%, #06b6d4 100%);
83
+ --gradient-glass: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.4) 100%);
84
+ --gradient-overlay: linear-gradient(180deg, rgba(10, 14, 26, 0) 0%, rgba(10, 14, 26, 0.8) 100%);
85
+
86
+ /* Radial Gradients for Background */
87
+ --gradient-radial-blue: radial-gradient(circle at 20% 30%, rgba(59, 130, 246, 0.15) 0%, transparent 40%);
88
+ --gradient-radial-purple: radial-gradient(circle at 80% 70%, rgba(139, 92, 246, 0.15) 0%, transparent 40%);
89
+ --gradient-radial-green: radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.1) 0%, transparent 30%);
90
+
91
+ /* ===== TYPOGRAPHY ===== */
92
+ --font-family-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
93
+ --font-family-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
94
 
95
  /* Font Sizes */
96
  --font-size-xs: 0.75rem; /* 12px */
 
102
  --font-size-2xl: 1.875rem; /* 30px */
103
  --font-size-3xl: 2.25rem; /* 36px */
104
  --font-size-4xl: 3rem; /* 48px */
 
105
 
106
  /* Font Weights */
107
  --font-weight-light: 300;
 
114
 
115
  /* Line Heights */
116
  --line-height-tight: 1.25;
 
117
  --line-height-normal: 1.5;
118
+ --line-height-relaxed: 1.75;
119
+ --line-height-loose: 2;
 
 
 
 
 
 
 
 
 
120
 
121
  /* ===== SPACING SCALE ===== */
122
+ --spacing-0: 0;
123
+ --spacing-1: 0.25rem; /* 4px */
124
+ --spacing-2: 0.5rem; /* 8px */
125
+ --spacing-3: 0.75rem; /* 12px */
126
+ --spacing-4: 1rem; /* 16px */
127
+ --spacing-5: 1.25rem; /* 20px */
128
+ --spacing-6: 1.5rem; /* 24px */
129
+ --spacing-8: 2rem; /* 32px */
130
+ --spacing-10: 2.5rem; /* 40px */
131
+ --spacing-12: 3rem; /* 48px */
132
+ --spacing-16: 4rem; /* 64px */
133
+ --spacing-20: 5rem; /* 80px */
 
 
 
134
 
135
  /* Semantic Spacing */
136
+ --spacing-xs: var(--spacing-1);
137
+ --spacing-sm: var(--spacing-2);
138
+ --spacing-md: var(--spacing-4);
139
+ --spacing-lg: var(--spacing-6);
140
+ --spacing-xl: var(--spacing-8);
141
+ --spacing-2xl: var(--spacing-12);
 
142
 
143
  /* ===== BORDER RADIUS ===== */
144
  --radius-none: 0;
145
+ --radius-sm: 0.25rem; /* 4px */
 
146
  --radius-base: 0.5rem; /* 8px */
147
  --radius-md: 0.75rem; /* 12px */
148
  --radius-lg: 1rem; /* 16px */
149
+ --radius-xl: 1.25rem; /* 20px */
150
+ --radius-2xl: 1.5rem; /* 24px */
151
+ --radius-3xl: 2rem; /* 32px */
152
  --radius-full: 9999px;
153
 
154
+ /* ===== SHADOWS ===== */
155
+ --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
156
+ --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
157
+ --shadow-base: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
158
+ --shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
159
+ --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
160
+ --shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
161
+ --shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
162
+
163
+ /* Colored Shadows */
164
+ --shadow-blue: 0 10px 30px -5px rgba(59, 130, 246, 0.3);
165
+ --shadow-purple: 0 10px 30px -5px rgba(139, 92, 246, 0.3);
166
+ --shadow-pink: 0 10px 30px -5px rgba(236, 72, 153, 0.3);
167
+ --shadow-green: 0 10px 30px -5px rgba(16, 185, 129, 0.3);
 
 
 
 
 
 
 
 
 
168
 
169
  /* Inner Shadows */
170
+ --shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
171
+ --shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.1);
172
 
173
+ /* ===== BLUR TOKENS ===== */
174
  --blur-none: 0;
 
175
  --blur-sm: 4px;
176
  --blur-base: 8px;
177
  --blur-md: 12px;
178
  --blur-lg: 16px;
179
+ --blur-xl: 20px;
180
  --blur-2xl: 40px;
181
  --blur-3xl: 64px;
182
 
183
+ /* ===== ELEVATION LEVELS ===== */
184
+ /* Use these for layering UI elements */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  --z-base: 0;
186
  --z-dropdown: 1000;
187
  --z-sticky: 1020;
 
191
  --z-popover: 1060;
192
  --z-tooltip: 1070;
193
  --z-notification: 1080;
 
194
 
195
+ /* ===== ANIMATION TIMINGS ===== */
196
+ --duration-instant: 0ms;
197
+ --duration-fast: 150ms;
198
+ --duration-base: 250ms;
199
+ --duration-slow: 350ms;
200
+ --duration-slower: 500ms;
201
+
202
+ /* Easing Functions */
203
+ --ease-linear: linear;
204
+ --ease-in: cubic-bezier(0.4, 0, 1, 1);
205
+ --ease-out: cubic-bezier(0, 0, 0.2, 1);
206
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
207
+ --ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
208
+
209
+ /* ===== LAYOUT ===== */
210
  --header-height: 72px;
211
  --sidebar-width: 280px;
212
  --sidebar-collapsed-width: 80px;
213
  --mobile-nav-height: 64px;
214
+
215
  --container-max-width: 1920px;
216
  --content-max-width: 1440px;
217
 
 
223
  --breakpoint-xl: 1024px;
224
  --breakpoint-2xl: 1280px;
225
  --breakpoint-3xl: 1440px;
 
226
  }
227
 
228
+ /* ===== LIGHT MODE OVERRIDES ===== */
229
  [data-theme="light"] {
230
+ --color-bg-primary: #ffffff;
231
+ --color-bg-secondary: #f9fafb;
232
+ --color-bg-tertiary: #f3f4f6;
233
+ --color-bg-elevated: #ffffff;
234
+ --color-bg-overlay: rgba(255, 255, 255, 0.9);
235
+
236
+ --color-glass-bg: rgba(255, 255, 255, 0.7);
237
+ --color-glass-bg-light: rgba(249, 250, 251, 0.5);
238
+ --color-glass-border: rgba(0, 0, 0, 0.1);
239
+
240
+ --color-text-primary: #111827;
241
+ --color-text-secondary: #6b7280;
242
+ --color-text-tertiary: #9ca3af;
243
+ --color-text-disabled: #d1d5db;
244
+ --color-text-inverse: #ffffff;
245
+
246
+ --color-border-primary: rgba(0, 0, 0, 0.1);
247
+ --color-border-secondary: rgba(0, 0, 0, 0.05);
248
+
249
+ --gradient-glass: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(249, 250, 251, 0.4) 100%);
250
+ --gradient-overlay: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%);
251
+
252
+ --shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
253
+ --shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.08), 0 1px 2px 0 rgba(0, 0, 0, 0.04);
254
+ --shadow-base: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
255
+ --shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
256
+ --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  }
258
 
259
  /* ===== UTILITY CLASSES ===== */
260
 
261
  /* Glassmorphism Effects */
262
  .glass-effect {
263
+ background: var(--color-glass-bg);
264
+ backdrop-filter: blur(var(--blur-xl));
265
+ border: 1px solid var(--color-glass-border);
 
266
  }
267
 
268
  .glass-effect-light {
269
+ background: var(--color-glass-bg-light);
270
+ backdrop-filter: blur(var(--blur-lg));
271
+ border: 1px solid var(--color-glass-border);
 
 
 
 
 
 
 
 
272
  }
273
 
274
  /* Gradient Backgrounds */
 
276
  background: var(--gradient-primary);
277
  }
278
 
279
+ .bg-gradient-secondary {
280
+ background: var(--gradient-secondary);
 
 
 
 
281
  }
282
 
283
  /* Text Gradients */
 
288
  -webkit-text-fill-color: transparent;
289
  }
290
 
 
 
 
 
 
 
 
291
  /* Shadow Utilities */
292
  .shadow-glow-blue {
293
  box-shadow: var(--shadow-blue);
 
307
 
308
  /* Animation Utilities */
309
  .transition-fast {
310
+ transition: all var(--duration-fast) var(--ease-out);
311
  }
312
 
313
  .transition-base {
314
+ transition: all var(--duration-base) var(--ease-in-out);
315
  }
316
 
317
  .transition-slow {
318
+ transition: all var(--duration-slow) var(--ease-in-out);
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
static/css/enhancements.css ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Additional UI Enhancements for Pro Trading Terminal */
2
+
3
+ /* Glassmorphism Effects */
4
+ .glass-card {
5
+ background: rgba(17, 24, 39, 0.4);
6
+ backdrop-filter: blur(20px) saturate(180%);
7
+ border: 1px solid rgba(255, 255, 255, 0.1);
8
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
9
+ }
10
+
11
+ /* Neon Glow Effects */
12
+ .neon-text {
13
+ text-shadow:
14
+ 0 0 5px rgba(102, 126, 234, 0.5),
15
+ 0 0 10px rgba(102, 126, 234, 0.3),
16
+ 0 0 20px rgba(102, 126, 234, 0.2);
17
+ }
18
+
19
+ .neon-border {
20
+ border: 1px solid var(--primary);
21
+ box-shadow:
22
+ 0 0 5px rgba(102, 126, 234, 0.5),
23
+ 0 0 10px rgba(102, 126, 234, 0.3),
24
+ inset 0 0 10px rgba(102, 126, 234, 0.1);
25
+ }
26
+
27
+ /* Price Movement Animations */
28
+ .price-up {
29
+ color: var(--success) !important;
30
+ animation: priceFlash 0.5s ease;
31
+ }
32
+
33
+ .price-down {
34
+ color: var(--danger) !important;
35
+ animation: priceFlash 0.5s ease;
36
+ }
37
+
38
+ @keyframes priceFlash {
39
+ 0%, 100% { opacity: 1; }
40
+ 50% { opacity: 0.6; transform: scale(1.05); }
41
+ }
42
+
43
+ /* Market Data Table Enhancements */
44
+ .market-table {
45
+ width: 100%;
46
+ border-collapse: separate;
47
+ border-spacing: 0;
48
+ }
49
+
50
+ .market-table thead {
51
+ position: sticky;
52
+ top: 0;
53
+ z-index: 10;
54
+ background: rgba(17, 24, 39, 0.95);
55
+ backdrop-filter: blur(10px);
56
+ }
57
+
58
+ .market-table th {
59
+ padding: 15px 12px;
60
+ text-align: left;
61
+ font-weight: 600;
62
+ font-size: 12px;
63
+ text-transform: uppercase;
64
+ letter-spacing: 0.5px;
65
+ color: var(--text-secondary);
66
+ border-bottom: 2px solid var(--border);
67
+ }
68
+
69
+ .market-table td {
70
+ padding: 15px 12px;
71
+ border-bottom: 1px solid var(--border-light);
72
+ font-size: 14px;
73
+ }
74
+
75
+ .market-table tbody tr {
76
+ transition: all 0.2s;
77
+ }
78
+
79
+ .market-table tbody tr:hover {
80
+ background: rgba(102, 126, 234, 0.05);
81
+ transform: scale(1.01);
82
+ }
83
+
84
+ .market-table .coin-info {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 10px;
88
+ }
89
+
90
+ .market-table .coin-icon {
91
+ width: 32px;
92
+ height: 32px;
93
+ border-radius: 50%;
94
+ object-fit: cover;
95
+ }
96
+
97
+ .market-table .coin-symbol {
98
+ font-weight: 700;
99
+ color: var(--text-primary);
100
+ }
101
+
102
+ .market-table .coin-name {
103
+ font-size: 12px;
104
+ color: var(--text-secondary);
105
+ }
106
+
107
+ .market-table .price {
108
+ font-family: 'JetBrains Mono', monospace;
109
+ font-weight: 600;
110
+ font-size: 15px;
111
+ }
112
+
113
+ .market-table .change-positive {
114
+ color: var(--success);
115
+ font-weight: 600;
116
+ }
117
+
118
+ .market-table .change-negative {
119
+ color: var(--danger);
120
+ font-weight: 600;
121
+ }
122
+
123
+ /* Chart Container Enhancements */
124
+ .chart-container {
125
+ position: relative;
126
+ height: 300px;
127
+ padding: 20px;
128
+ background: rgba(17, 24, 39, 0.4);
129
+ border-radius: 12px;
130
+ border: 1px solid var(--border);
131
+ }
132
+
133
+ .chart-container canvas {
134
+ max-height: 100%;
135
+ }
136
+
137
+ /* Sentiment Visualization */
138
+ .sentiment-meter {
139
+ width: 100%;
140
+ height: 20px;
141
+ background: linear-gradient(90deg,
142
+ var(--danger) 0%,
143
+ var(--warning) 25%,
144
+ var(--text-secondary) 50%,
145
+ var(--info) 75%,
146
+ var(--success) 100%
147
+ );
148
+ border-radius: 10px;
149
+ position: relative;
150
+ overflow: hidden;
151
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2);
152
+ }
153
+
154
+ .sentiment-indicator {
155
+ position: absolute;
156
+ top: -5px;
157
+ width: 30px;
158
+ height: 30px;
159
+ background: white;
160
+ border: 3px solid var(--primary);
161
+ border-radius: 50%;
162
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
163
+ transition: left 0.5s ease;
164
+ }
165
+
166
+ /* Model Status Grid */
167
+ .model-grid {
168
+ display: grid;
169
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
170
+ gap: 15px;
171
+ }
172
+
173
+ .model-card {
174
+ background: rgba(17, 24, 39, 0.6);
175
+ border: 1px solid var(--border);
176
+ border-radius: 12px;
177
+ padding: 20px;
178
+ transition: all 0.3s;
179
+ position: relative;
180
+ overflow: hidden;
181
+ }
182
+
183
+ .model-card::before {
184
+ content: '';
185
+ position: absolute;
186
+ top: 0;
187
+ left: 0;
188
+ width: 100%;
189
+ height: 3px;
190
+ background: var(--gradient-purple);
191
+ transform: scaleX(0);
192
+ transition: transform 0.3s;
193
+ }
194
+
195
+ .model-card:hover::before {
196
+ transform: scaleX(1);
197
+ }
198
+
199
+ .model-card:hover {
200
+ border-color: var(--primary);
201
+ transform: translateY(-3px);
202
+ box-shadow: 0 10px 30px rgba(102, 126, 234, 0.2);
203
+ }
204
+
205
+ .model-card-header {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ margin-bottom: 15px;
210
+ }
211
+
212
+ .model-name {
213
+ font-weight: 600;
214
+ font-size: 14px;
215
+ color: var(--text-primary);
216
+ }
217
+
218
+ .model-badge {
219
+ padding: 4px 8px;
220
+ border-radius: 6px;
221
+ font-size: 11px;
222
+ font-weight: 600;
223
+ text-transform: uppercase;
224
+ }
225
+
226
+ .model-badge.loaded {
227
+ background: rgba(16, 185, 129, 0.2);
228
+ color: var(--success);
229
+ }
230
+
231
+ .model-badge.failed {
232
+ background: rgba(239, 68, 68, 0.2);
233
+ color: var(--danger);
234
+ }
235
+
236
+ .model-badge.loading {
237
+ background: rgba(245, 158, 11, 0.2);
238
+ color: var(--warning);
239
+ }
240
+
241
+ /* News Card */
242
+ .news-card {
243
+ background: rgba(17, 24, 39, 0.6);
244
+ border: 1px solid var(--border);
245
+ border-radius: 12px;
246
+ padding: 20px;
247
+ margin-bottom: 15px;
248
+ transition: all 0.3s;
249
+ cursor: pointer;
250
+ }
251
+
252
+ .news-card:hover {
253
+ border-color: var(--primary);
254
+ transform: translateX(5px);
255
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.2);
256
+ }
257
+
258
+ .news-card-header {
259
+ display: flex;
260
+ justify-content: space-between;
261
+ align-items: flex-start;
262
+ margin-bottom: 10px;
263
+ }
264
+
265
+ .news-title {
266
+ font-weight: 600;
267
+ font-size: 16px;
268
+ color: var(--text-primary);
269
+ margin-bottom: 8px;
270
+ line-height: 1.4;
271
+ }
272
+
273
+ .news-meta {
274
+ display: flex;
275
+ gap: 15px;
276
+ font-size: 12px;
277
+ color: var(--text-secondary);
278
+ margin-bottom: 10px;
279
+ }
280
+
281
+ .news-source {
282
+ font-weight: 600;
283
+ color: var(--primary);
284
+ }
285
+
286
+ .news-content {
287
+ font-size: 14px;
288
+ color: var(--text-secondary);
289
+ line-height: 1.6;
290
+ margin-bottom: 10px;
291
+ }
292
+
293
+ /* API Explorer */
294
+ .api-request-panel {
295
+ background: rgba(17, 24, 39, 0.6);
296
+ border: 1px solid var(--border);
297
+ border-radius: 12px;
298
+ padding: 20px;
299
+ margin-bottom: 20px;
300
+ }
301
+
302
+ .api-response {
303
+ background: rgba(0, 0, 0, 0.4);
304
+ border: 1px solid var(--border);
305
+ border-radius: 8px;
306
+ padding: 15px;
307
+ font-family: 'JetBrains Mono', monospace;
308
+ font-size: 13px;
309
+ color: #a5d6ff;
310
+ max-height: 500px;
311
+ overflow-y: auto;
312
+ white-space: pre-wrap;
313
+ word-wrap: break-word;
314
+ }
315
+
316
+ .api-response .json-key {
317
+ color: #79c0ff;
318
+ }
319
+
320
+ .api-response .json-string {
321
+ color: #a5d6ff;
322
+ }
323
+
324
+ .api-response .json-number {
325
+ color: #79c0ff;
326
+ }
327
+
328
+ .api-response .json-boolean {
329
+ color: #ff7b72;
330
+ }
331
+
332
+ /* Skeleton Loading */
333
+ .skeleton {
334
+ background: linear-gradient(
335
+ 90deg,
336
+ rgba(255, 255, 255, 0.05) 25%,
337
+ rgba(255, 255, 255, 0.1) 50%,
338
+ rgba(255, 255, 255, 0.05) 75%
339
+ );
340
+ background-size: 200% 100%;
341
+ animation: skeleton-loading 1.5s infinite;
342
+ border-radius: 8px;
343
+ }
344
+
345
+ @keyframes skeleton-loading {
346
+ 0% { background-position: 200% 0; }
347
+ 100% { background-position: -200% 0; }
348
+ }
349
+
350
+ .skeleton-text {
351
+ height: 16px;
352
+ margin-bottom: 10px;
353
+ }
354
+
355
+ .skeleton-title {
356
+ height: 24px;
357
+ width: 60%;
358
+ margin-bottom: 15px;
359
+ }
360
+
361
+ /* Empty State */
362
+ .empty-state {
363
+ text-align: center;
364
+ padding: 60px 20px;
365
+ color: var(--text-secondary);
366
+ }
367
+
368
+ .empty-state-icon {
369
+ font-size: 64px;
370
+ margin-bottom: 20px;
371
+ opacity: 0.5;
372
+ }
373
+
374
+ .empty-state-title {
375
+ font-size: 20px;
376
+ font-weight: 600;
377
+ margin-bottom: 10px;
378
+ color: var(--text-primary);
379
+ }
380
+
381
+ .empty-state-message {
382
+ font-size: 14px;
383
+ margin-bottom: 20px;
384
+ }
385
+
386
+ /* Pulse Animation for Live Data */
387
+ .live-indicator {
388
+ display: inline-flex;
389
+ align-items: center;
390
+ gap: 6px;
391
+ padding: 4px 10px;
392
+ background: rgba(239, 68, 68, 0.15);
393
+ border: 1px solid rgba(239, 68, 68, 0.3);
394
+ border-radius: 6px;
395
+ font-size: 11px;
396
+ font-weight: 600;
397
+ text-transform: uppercase;
398
+ }
399
+
400
+ .live-dot {
401
+ width: 8px;
402
+ height: 8px;
403
+ background: var(--danger);
404
+ border-radius: 50%;
405
+ animation: pulse 2s infinite;
406
+ }
407
+
408
+ /* Responsive Grid Improvements */
409
+ @media (max-width: 1200px) {
410
+ .model-grid {
411
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
412
+ }
413
+ }
414
+
415
+ @media (max-width: 768px) {
416
+ .chart-container {
417
+ height: 250px;
418
+ padding: 15px;
419
+ }
420
+
421
+ .market-table th,
422
+ .market-table td {
423
+ padding: 10px 8px;
424
+ font-size: 12px;
425
+ }
426
+
427
+ .market-table .coin-icon {
428
+ width: 24px;
429
+ height: 24px;
430
+ }
431
+
432
+ .model-grid {
433
+ grid-template-columns: 1fr;
434
+ }
435
+
436
+ .news-card {
437
+ padding: 15px;
438
+ }
439
+ }
440
+
static/css/enterprise-components.css CHANGED
@@ -290,19 +290,14 @@
290
  }
291
 
292
  .btn-secondary {
293
- background: var(--color-glass-bg);
294
  color: var(--color-text-primary);
295
  border-color: var(--color-border-primary);
296
- font-weight: 600;
297
- opacity: 0.9;
298
  }
299
 
300
  .btn-secondary:hover:not(:disabled) {
301
- background: var(--color-glass-bg-strong);
302
  border-color: var(--color-accent-blue);
303
- color: var(--color-text-primary);
304
- opacity: 1;
305
- box-shadow: 0 2px 8px rgba(6, 182, 212, 0.2);
306
  }
307
 
308
  .btn-success {
 
290
  }
291
 
292
  .btn-secondary {
293
+ background: transparent;
294
  color: var(--color-text-primary);
295
  border-color: var(--color-border-primary);
 
 
296
  }
297
 
298
  .btn-secondary:hover:not(:disabled) {
299
+ background: var(--color-glass-bg);
300
  border-color: var(--color-accent-blue);
 
 
 
301
  }
302
 
303
  .btn-success {
static/css/pro-dashboard.css CHANGED
@@ -1,68 +1,22 @@
1
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
2
 
3
  :root {
4
- /* ===== UNIFIED COLOR PALETTE - Professional & Harmonious ===== */
5
- --bg-gradient: radial-gradient(circle at top, #0a0e1a, #05060a 70%);
6
-
7
- /* Primary Colors - Blue/Purple Harmony */
8
- --primary: #818CF8;
9
- --primary-strong: #6366F1;
10
- --primary-light: #A5B4FC;
11
- --primary-dark: #4F46E5;
12
- --primary-glow: rgba(129, 140, 248, 0.4);
13
-
14
- /* Secondary Colors - Cyan/Teal Harmony */
15
- --secondary: #22D3EE;
16
- --secondary-light: #67E8F9;
17
- --secondary-dark: #06B6D4;
18
- --secondary-glow: rgba(34, 211, 238, 0.4);
19
-
20
- /* Accent Colors - Pink/Magenta */
21
- --accent: #F472B6;
22
- --accent-light: #F9A8D4;
23
- --accent-dark: #EC4899;
24
-
25
- /* Status Colors - Bright & Professional */
26
- --success: #34D399;
27
- --success-light: #6EE7B7;
28
- --success-dark: #10B981;
29
- --success-glow: rgba(52, 211, 153, 0.5);
30
-
31
- --warning: #FBBF24;
32
- --warning-light: #FCD34D;
33
- --warning-dark: #F59E0B;
34
-
35
- --danger: #F87171;
36
- --danger-light: #FCA5A5;
37
- --danger-dark: #EF4444;
38
-
39
- --info: #60A5FA;
40
- --info-light: #93C5FD;
41
- --info-dark: #3B82F6;
42
-
43
- /* Glass Morphism - Unified */
44
- --glass-bg: rgba(30, 41, 59, 0.85);
45
- --glass-bg-light: rgba(30, 41, 59, 0.6);
46
- --glass-bg-strong: rgba(30, 41, 59, 0.95);
47
- --glass-border: rgba(255, 255, 255, 0.15);
48
- --glass-border-light: rgba(255, 255, 255, 0.1);
49
- --glass-border-strong: rgba(255, 255, 255, 0.25);
50
- --glass-highlight: rgba(255, 255, 255, 0.2);
51
-
52
- /* Text Colors - Consistent Hierarchy */
53
- --text-primary: #F8FAFC;
54
- --text-secondary: #E2E8F0;
55
- --text-soft: #CBD5E1;
56
- --text-muted: rgba(226, 232, 240, 0.75);
57
- --text-faint: rgba(226, 232, 240, 0.5);
58
-
59
- /* Shadows - Unified */
60
- --shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.7);
61
- --shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.6);
62
- --shadow-glow-primary: 0 0 40px rgba(129, 140, 248, 0.2);
63
- --shadow-glow-secondary: 0 0 40px rgba(34, 211, 238, 0.2);
64
-
65
- /* Layout */
66
  --sidebar-width: 260px;
67
  }
68
 
@@ -74,115 +28,18 @@ html, body {
74
  margin: 0;
75
  padding: 0;
76
  min-height: 100vh;
77
- font-family: 'Manrope', 'DM Sans', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
78
- font-weight: 500;
79
- font-size: 15px;
80
- line-height: 1.65;
81
- letter-spacing: -0.015em;
82
  background: var(--bg-gradient);
83
  color: var(--text-primary);
84
- -webkit-font-smoothing: antialiased;
85
- -moz-osx-font-smoothing: grayscale;
86
- text-rendering: optimizeLegibility;
87
  }
88
 
89
  body[data-theme='light'] {
90
  --bg-gradient: radial-gradient(circle at top, #f3f6ff, #dfe5ff);
91
-
92
- /* Glass Morphism - Light */
93
- --glass-bg: rgba(255, 255, 255, 0.85);
94
- --glass-bg-light: rgba(255, 255, 255, 0.7);
95
- --glass-bg-strong: rgba(255, 255, 255, 0.95);
96
- --glass-border: rgba(15, 23, 42, 0.15);
97
- --glass-border-light: rgba(15, 23, 42, 0.1);
98
- --glass-border-strong: rgba(15, 23, 42, 0.25);
99
- --glass-highlight: rgba(15, 23, 42, 0.08);
100
-
101
- /* Text Colors - Light */
102
  --text-primary: #0f172a;
103
- --text-secondary: #1e293b;
104
- --text-soft: #334155;
105
- --text-muted: rgba(15, 23, 42, 0.7);
106
- --text-faint: rgba(15, 23, 42, 0.5);
107
-
108
- /* Shadows - Light */
109
- --shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.15);
110
- --shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.1);
111
- --shadow-glow-primary: 0 0 40px rgba(96, 165, 250, 0.3);
112
- --shadow-glow-secondary: 0 0 40px rgba(34, 211, 238, 0.3);
113
- }
114
-
115
- /* Light Theme Sidebar Styles */
116
- body[data-theme='light'] .sidebar {
117
- background: linear-gradient(180deg,
118
- rgba(255, 255, 255, 0.95) 0%,
119
- rgba(248, 250, 252, 0.98) 50%,
120
- rgba(255, 255, 255, 0.95) 100%);
121
- border-right: 2px solid rgba(96, 165, 250, 0.2);
122
- box-shadow:
123
- 8px 0 32px rgba(0, 0, 0, 0.08),
124
- inset -2px 0 0 rgba(96, 165, 250, 0.15),
125
- 0 0 60px rgba(96, 165, 250, 0.05);
126
- }
127
-
128
- body[data-theme='light'] .sidebar::before {
129
- background: linear-gradient(90deg, transparent, rgba(96, 165, 250, 0.3), rgba(34, 211, 238, 0.25), transparent);
130
- opacity: 0.5;
131
- }
132
-
133
- body[data-theme='light'] .sidebar::after {
134
- background: linear-gradient(90deg, transparent, rgba(96, 165, 250, 0.15), transparent);
135
- opacity: 0.3;
136
- }
137
-
138
- body[data-theme='light'] .nav-button {
139
- color: rgba(15, 23, 42, 0.8);
140
- }
141
-
142
- body[data-theme='light'] .nav-button:hover {
143
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.15), rgba(34, 211, 238, 0.12));
144
- color: #0f172a;
145
- }
146
-
147
- body[data-theme='light'] .nav-button.active {
148
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(34, 211, 238, 0.18));
149
- color: #0f172a;
150
- border: 1px solid rgba(96, 165, 250, 0.3);
151
- }
152
-
153
- body[data-theme='light'] .nav-button::before {
154
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(34, 211, 238, 0.18));
155
- border: 2.5px solid rgba(96, 165, 250, 0.4);
156
- }
157
-
158
- body[data-theme='light'] .nav-button.active::before {
159
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.3), rgba(34, 211, 238, 0.25));
160
- border-color: rgba(96, 165, 250, 0.6);
161
- }
162
-
163
- body[data-theme='light'] .brand {
164
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.1), rgba(34, 211, 238, 0.08));
165
- border: 2px solid rgba(96, 165, 250, 0.2);
166
- }
167
-
168
- body[data-theme='light'] .brand:hover {
169
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.15), rgba(34, 211, 238, 0.12));
170
- border-color: rgba(96, 165, 250, 0.3);
171
- }
172
-
173
- body[data-theme='light'] .brand-icon {
174
- background: linear-gradient(135deg, rgba(96, 165, 250, 0.15), rgba(34, 211, 238, 0.12));
175
- border: 2px solid rgba(96, 165, 250, 0.3);
176
- }
177
-
178
- body[data-theme='light'] .sidebar-footer {
179
- border-top: 1px solid rgba(96, 165, 250, 0.15);
180
- }
181
-
182
- body[data-theme='light'] .footer-badge {
183
- background: rgba(96, 165, 250, 0.1);
184
- border: 1px solid rgba(96, 165, 250, 0.2);
185
- color: rgba(15, 23, 42, 0.8);
186
  }
187
 
188
  .app-shell {
@@ -192,257 +49,39 @@ body[data-theme='light'] .footer-badge {
192
 
193
  .sidebar {
194
  width: var(--sidebar-width);
195
- padding: 24px 16px;
196
- background: linear-gradient(180deg,
197
- rgba(10, 15, 30, 0.98) 0%,
198
- rgba(15, 23, 42, 0.96) 50%,
199
- rgba(10, 15, 30, 0.98) 100%);
200
- backdrop-filter: blur(40px) saturate(200%);
201
- border-right: 2px solid rgba(129, 140, 248, 0.3);
202
  display: flex;
203
  flex-direction: column;
204
  gap: 24px;
205
  position: sticky;
206
  top: 0;
207
  height: 100vh;
208
- box-shadow:
209
- 8px 0 32px rgba(0, 0, 0, 0.6),
210
- inset -2px 0 0 rgba(129, 140, 248, 0.2),
211
- 0 0 60px rgba(129, 140, 248, 0.1);
212
- z-index: 100;
213
- transition: border-color 0.3s ease, box-shadow 0.3s ease;
214
- position: relative;
215
- }
216
-
217
- .sidebar::before {
218
- content: '';
219
- position: absolute;
220
- top: 0;
221
- left: 0;
222
- right: 0;
223
- height: 2px;
224
- background: linear-gradient(90deg, transparent, rgba(129, 140, 248, 0.4), rgba(34, 211, 238, 0.3), transparent);
225
- opacity: 0.6;
226
- }
227
-
228
- .sidebar::after {
229
- content: '';
230
- position: absolute;
231
- bottom: 0;
232
- left: 0;
233
- right: 0;
234
- height: 1px;
235
- background: linear-gradient(90deg, transparent, rgba(129, 140, 248, 0.2), transparent);
236
- opacity: 0.4;
237
  }
238
 
239
  .brand {
240
- display: flex;
241
- align-items: center;
242
- gap: 14px;
243
- padding: 18px 16px;
244
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.15), rgba(34, 211, 238, 0.1));
245
- border-radius: 18px;
246
- border: 2px solid rgba(129, 140, 248, 0.3);
247
- box-shadow:
248
- inset 0 2px 4px rgba(255, 255, 255, 0.15),
249
- inset 0 -2px 4px rgba(0, 0, 0, 0.2),
250
- 0 4px 16px rgba(0, 0, 0, 0.4),
251
- 0 0 30px rgba(129, 140, 248, 0.2);
252
- position: relative;
253
- overflow: hidden;
254
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
255
- backdrop-filter: blur(20px) saturate(180%);
256
- }
257
-
258
- .brand::before {
259
- content: '';
260
- position: absolute;
261
- inset: 0;
262
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.2), rgba(34, 211, 238, 0.15));
263
- opacity: 0;
264
- transition: opacity 0.4s ease;
265
- }
266
-
267
- .brand::after {
268
- content: '';
269
- position: absolute;
270
- top: -50%;
271
- left: -50%;
272
- width: 200%;
273
- height: 200%;
274
- background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
275
- transform: rotate(45deg);
276
- transition: transform 0.6s ease;
277
- opacity: 0;
278
- }
279
-
280
- .brand:hover {
281
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.25), rgba(34, 211, 238, 0.2));
282
- border-color: rgba(129, 140, 248, 0.5);
283
- box-shadow:
284
- inset 0 2px 6px rgba(255, 255, 255, 0.2),
285
- inset 0 -2px 6px rgba(0, 0, 0, 0.3),
286
- 0 6px 24px rgba(129, 140, 248, 0.4),
287
- 0 0 40px rgba(129, 140, 248, 0.3);
288
- }
289
-
290
- .brand:hover::before {
291
- opacity: 1;
292
- }
293
-
294
- .brand:hover::after {
295
- opacity: 1;
296
- transform: rotate(45deg) translate(100%, 100%);
297
- transition: transform 0.8s ease;
298
- }
299
-
300
- .brand-icon {
301
- display: flex;
302
- align-items: center;
303
- justify-content: center;
304
- width: 52px;
305
- height: 52px;
306
- border-radius: 50%;
307
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.25), rgba(34, 211, 238, 0.2));
308
- border: 2px solid rgba(129, 140, 248, 0.4);
309
- color: var(--primary-light);
310
- flex-shrink: 0;
311
- box-shadow:
312
- inset 0 2px 4px rgba(255, 255, 255, 0.2),
313
- inset 0 -2px 4px rgba(0, 0, 0, 0.3),
314
- 0 4px 12px rgba(129, 140, 248, 0.3),
315
- 0 0 20px rgba(129, 140, 248, 0.2);
316
- position: relative;
317
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
318
- backdrop-filter: blur(15px) saturate(180%);
319
- animation: brandIconPulse 3s ease-in-out infinite;
320
- }
321
-
322
- .brand-icon::before {
323
- content: '';
324
- position: absolute;
325
- inset: -2px;
326
- border-radius: 50%;
327
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.4), rgba(34, 211, 238, 0.3));
328
- opacity: 0;
329
- transition: opacity 0.4s ease;
330
- z-index: -1;
331
- filter: blur(8px);
332
- }
333
-
334
- .brand-icon::after {
335
- content: '';
336
- position: absolute;
337
- top: 50%;
338
- left: 50%;
339
- width: 0;
340
- height: 0;
341
- border-radius: 50%;
342
- background: radial-gradient(circle, rgba(255, 255, 255, 0.3), transparent);
343
- transform: translate(-50%, -50%);
344
- transition: width 0.4s ease, height 0.4s ease;
345
- opacity: 0;
346
- }
347
-
348
- .brand:hover .brand-icon {
349
- box-shadow:
350
- inset 0 2px 6px rgba(255, 255, 255, 0.25),
351
- inset 0 -2px 6px rgba(0, 0, 0, 0.3),
352
- 0 6px 20px rgba(129, 140, 248, 0.5),
353
- 0 0 30px rgba(129, 140, 248, 0.4);
354
- border-color: rgba(129, 140, 248, 0.6);
355
- }
356
-
357
- .brand:hover .brand-icon::before {
358
- opacity: 1;
359
- }
360
-
361
- .brand:hover .brand-icon::after {
362
- width: 100%;
363
- height: 100%;
364
- opacity: 1;
365
- }
366
-
367
- .brand-icon svg {
368
- position: relative;
369
- z-index: 1;
370
- filter: drop-shadow(0 2px 6px rgba(129, 140, 248, 0.6));
371
- transition: filter 0.4s ease;
372
- }
373
-
374
- .brand:hover .brand-icon svg {
375
- filter: drop-shadow(0 3px 10px rgba(129, 140, 248, 0.8));
376
- }
377
-
378
- @keyframes brandIconPulse {
379
- 0%, 100% {
380
- box-shadow:
381
- inset 0 2px 4px rgba(255, 255, 255, 0.2),
382
- inset 0 -2px 4px rgba(0, 0, 0, 0.3),
383
- 0 4px 12px rgba(129, 140, 248, 0.3),
384
- 0 0 20px rgba(129, 140, 248, 0.2);
385
- }
386
- 50% {
387
- box-shadow:
388
- inset 0 2px 4px rgba(255, 255, 255, 0.2),
389
- inset 0 -2px 4px rgba(0, 0, 0, 0.3),
390
- 0 4px 12px rgba(129, 140, 248, 0.4),
391
- 0 0 30px rgba(129, 140, 248, 0.3);
392
- }
393
- }
394
-
395
- .brand-text {
396
  display: flex;
397
  flex-direction: column;
398
  gap: 6px;
399
- flex: 1;
400
- min-width: 0;
401
  }
402
 
403
  .brand strong {
404
- font-size: 1.0625rem;
405
- font-weight: 800;
406
- letter-spacing: -0.02em;
407
- font-family: 'Manrope', 'DM Sans', sans-serif;
408
- color: var(--text-primary);
409
- line-height: 1.3;
410
- white-space: nowrap;
411
- overflow: hidden;
412
- text-overflow: ellipsis;
413
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
414
- -webkit-background-clip: text;
415
- -webkit-text-fill-color: transparent;
416
- background-clip: text;
417
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2));
418
- transition: all 0.3s ease;
419
- }
420
-
421
- .brand:hover strong {
422
- background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
423
- -webkit-background-clip: text;
424
- -webkit-text-fill-color: transparent;
425
- background-clip: text;
426
  }
427
 
428
  .env-pill {
429
  display: inline-flex;
430
  align-items: center;
431
- gap: 5px;
432
- background: rgba(143, 136, 255, 0.1);
433
- border: 1px solid rgba(143, 136, 255, 0.2);
434
- padding: 3px 8px;
435
- border-radius: 6px;
436
- font-size: 0.65rem;
437
- font-weight: 600;
438
  text-transform: uppercase;
439
- letter-spacing: 0.06em;
440
- color: rgba(143, 136, 255, 0.9);
441
- font-family: 'Manrope', sans-serif;
442
- white-space: nowrap;
443
- overflow: hidden;
444
- text-overflow: ellipsis;
445
- max-width: 100%;
446
  }
447
 
448
  .nav {
@@ -454,2769 +93,457 @@ body[data-theme='light'] .footer-badge {
454
  .nav-button {
455
  border: none;
456
  border-radius: 14px;
457
- padding: 14px 18px;
458
  display: flex;
459
  align-items: center;
460
- gap: 14px;
461
  background: transparent;
462
- color: rgba(226, 232, 240, 0.8);
463
- font-weight: 600;
464
- font-family: 'Manrope', sans-serif;
465
- font-size: 0.9375rem;
466
  cursor: pointer;
467
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
468
- position: relative;
469
- overflow: visible;
470
  }
471
 
472
- .nav-button {
473
- position: relative;
 
 
474
  }
475
 
476
- .nav-button svg {
477
- width: 26px;
478
- height: 26px;
479
- flex-shrink: 0;
480
- filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6));
481
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
482
- z-index: 2;
483
- position: relative;
484
- opacity: 0.9;
485
- stroke-width: 2.5;
486
  }
487
 
488
- .nav-button::before {
489
- content: '';
490
- position: absolute;
491
- left: 0;
492
- width: 56px;
493
- height: 56px;
494
- border-radius: 50%;
495
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.3), rgba(34, 211, 238, 0.25));
496
- border: 2.5px solid rgba(129, 140, 248, 0.5);
497
- backdrop-filter: blur(25px) saturate(200%);
498
- opacity: 0;
499
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
500
- z-index: 0;
501
- box-shadow:
502
- inset 0 3px 8px rgba(255, 255, 255, 0.25),
503
- inset 0 -3px 8px rgba(0, 0, 0, 0.3),
504
- 0 6px 20px rgba(129, 140, 248, 0.4),
505
- 0 0 40px rgba(129, 140, 248, 0.3);
506
  }
507
 
508
- .nav-button:hover::before {
509
- opacity: 1;
510
- transform: scale(1.05);
511
- box-shadow:
512
- inset 0 3px 10px rgba(255, 255, 255, 0.3),
513
- inset 0 -3px 10px rgba(0, 0, 0, 0.35),
514
- 0 8px 24px rgba(129, 140, 248, 0.5),
515
- 0 0 50px rgba(129, 140, 248, 0.4);
516
- border-color: rgba(129, 140, 248, 0.7);
517
  }
518
 
519
- .nav-button.active::before {
520
- opacity: 1;
521
- transform: scale(1.1);
522
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.45), rgba(34, 211, 238, 0.4));
523
- border-color: rgba(129, 140, 248, 0.8);
524
- box-shadow:
525
- inset 0 4px 12px rgba(255, 255, 255, 0.35),
526
- inset 0 -4px 12px rgba(0, 0, 0, 0.4),
527
- 0 10px 30px rgba(129, 140, 248, 0.6),
528
- 0 0 60px rgba(129, 140, 248, 0.5),
529
- 0 0 80px rgba(34, 211, 238, 0.3);
 
530
  }
531
 
532
- .nav-button[data-nav="page-overview"] svg {
533
- color: #60A5FA;
534
- filter: drop-shadow(0 2px 4px rgba(96, 165, 250, 0.5));
535
  }
536
 
537
- .nav-button[data-nav="page-market"] svg {
538
- color: #A78BFA;
539
- filter: drop-shadow(0 2px 4px rgba(167, 139, 250, 0.5));
 
540
  }
541
 
542
- .nav-button[data-nav="page-chart"] svg {
543
- color: #F472B6;
544
- filter: drop-shadow(0 2px 4px rgba(244, 114, 182, 0.5));
 
 
 
 
 
 
 
 
545
  }
546
 
547
- .nav-button[data-nav="page-ai"] svg {
548
- color: #34D399;
549
- filter: drop-shadow(0 2px 4px rgba(52, 211, 153, 0.5));
 
 
550
  }
551
 
552
- .nav-button[data-nav="page-news"] svg {
553
- color: #FBBF24;
554
- filter: drop-shadow(0 2px 4px rgba(251, 191, 36, 0.5));
555
  }
556
 
557
- .nav-button[data-nav="page-providers"] svg {
558
- color: #22D3EE;
559
- filter: drop-shadow(0 2px 4px rgba(34, 211, 238, 0.5));
560
  }
561
 
562
- .nav-button[data-nav="page-api"] svg {
563
- color: #818CF8;
564
- filter: drop-shadow(0 2px 4px rgba(129, 140, 248, 0.5));
565
  }
566
 
567
- .nav-button[data-nav="page-debug"] svg {
568
- color: #F87171;
569
- filter: drop-shadow(0 2px 4px rgba(248, 113, 113, 0.5));
570
  }
571
 
572
- .nav-button[data-nav="page-datasets"] svg {
573
- color: #C084FC;
574
- filter: drop-shadow(0 2px 4px rgba(192, 132, 252, 0.5));
575
  }
576
 
577
- .nav-button[data-nav="page-settings"] svg {
578
- color: #94A3B8;
579
- filter: drop-shadow(0 2px 4px rgba(148, 163, 184, 0.5));
580
  }
581
 
582
- .nav-button::after {
583
- content: '';
584
- position: absolute;
585
- inset: 0;
586
- background: rgba(143, 136, 255, 0.05);
587
- border-radius: 10px;
588
- opacity: 0;
589
- transition: opacity 0.25s ease;
590
- z-index: -1;
591
  }
592
 
593
- .nav-button svg {
594
- width: 20px;
595
- height: 20px;
596
- fill: currentColor;
597
- stroke: currentColor;
598
- stroke-width: 2;
599
- transition: all 0.25s ease;
600
- flex-shrink: 0;
601
- opacity: 1;
602
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
603
  }
604
 
605
- .nav-button:hover {
606
- color: #ffffff;
607
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.3), rgba(34, 211, 238, 0.25));
608
- transform: translateX(4px);
609
- box-shadow:
610
- inset 0 2px 4px rgba(255, 255, 255, 0.15),
611
- 0 4px 16px rgba(129, 140, 248, 0.3),
612
- 0 0 25px rgba(129, 140, 248, 0.2);
613
  }
614
 
615
- .nav-button:hover svg {
616
- filter: drop-shadow(0 3px 10px rgba(129, 140, 248, 0.7));
617
- opacity: 1;
 
 
 
 
618
  }
619
 
620
- .nav-button:hover::before {
621
  opacity: 1;
622
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.35), rgba(34, 211, 238, 0.3));
623
- border-color: rgba(129, 140, 248, 0.6);
624
- box-shadow:
625
- inset 0 2px 6px rgba(255, 255, 255, 0.25),
626
- inset 0 -2px 6px rgba(0, 0, 0, 0.3),
627
- 0 6px 20px rgba(129, 140, 248, 0.4),
628
- 0 0 35px rgba(129, 140, 248, 0.3);
629
  }
630
 
631
- .nav-button:hover::after {
632
- opacity: 1;
633
- background: rgba(129, 140, 248, 0.12);
634
- }
635
-
636
- .nav-button.active {
637
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.2), rgba(34, 211, 238, 0.15));
638
- color: #ffffff;
639
- box-shadow:
640
- inset 0 2px 6px rgba(255, 255, 255, 0.15),
641
- 0 8px 24px rgba(129, 140, 248, 0.3),
642
- 0 0 40px rgba(129, 140, 248, 0.2);
643
- border: 1px solid rgba(129, 140, 248, 0.4);
644
- font-weight: 700;
645
- transform: translateX(6px);
646
  }
647
 
648
- .nav-button.active svg {
649
- filter: drop-shadow(0 4px 16px rgba(129, 140, 248, 0.9)) drop-shadow(0 0 20px rgba(34, 211, 238, 0.6));
650
- opacity: 1;
651
- transform: scale(1.1);
 
652
  }
653
 
654
- .nav-button.active::after {
655
- opacity: 1;
656
- background: rgba(129, 140, 248, 0.1);
 
657
  }
658
 
659
- .sidebar-footer {
660
- margin-top: auto;
661
- padding: 0;
662
  display: flex;
663
  align-items: center;
664
- justify-content: center;
 
665
  }
666
 
667
- .footer-badge {
668
- display: inline-flex;
669
- align-items: center;
670
- gap: 8px;
671
- padding: 10px 16px;
672
- background: rgba(255, 255, 255, 0.05);
673
- border: 1px solid rgba(255, 255, 255, 0.12);
674
- border-radius: 12px;
675
- font-size: 0.75rem;
676
- font-weight: 600;
677
- color: rgba(226, 232, 240, 0.8);
678
- font-family: 'Manrope', sans-serif;
679
- letter-spacing: 0.05em;
680
- text-transform: uppercase;
681
- transition: all 0.3s ease;
682
  }
683
 
684
- .footer-badge svg {
685
- width: 14px;
686
- height: 14px;
687
- opacity: 0.7;
688
- transition: all 0.3s ease;
689
  }
690
 
691
- .footer-badge:hover {
692
- background: rgba(255, 255, 255, 0.06);
693
- border-color: rgba(143, 136, 255, 0.3);
694
- color: var(--text-primary);
695
- transform: translateY(-2px);
696
  }
697
 
698
- .footer-badge:hover svg {
699
- opacity: 1;
700
- color: var(--primary);
701
- }
702
-
703
- .main-area {
704
- flex: 1;
705
- padding: 32px;
706
- display: flex;
707
- flex-direction: column;
708
- gap: 24px;
709
- }
710
-
711
- .topbar {
712
- display: flex;
713
- justify-content: space-between;
714
- align-items: center;
715
- padding: 28px 36px;
716
- border-radius: 24px;
717
- background: linear-gradient(135deg, var(--glass-bg-strong) 0%, var(--glass-bg) 100%);
718
- border: 1px solid var(--glass-border-strong);
719
- box-shadow:
720
- var(--shadow-strong),
721
- inset 0 1px 0 rgba(255, 255, 255, 0.15),
722
- var(--shadow-glow-primary),
723
- 0 0 60px rgba(129, 140, 248, 0.15);
724
- backdrop-filter: blur(30px) saturate(180%);
725
- flex-wrap: wrap;
726
- gap: 20px;
727
- position: relative;
728
- overflow: hidden;
729
- animation: headerGlow 4s ease-in-out infinite alternate;
730
- }
731
-
732
- @keyframes headerGlow {
733
- 0% {
734
- box-shadow:
735
- var(--shadow-strong),
736
- inset 0 1px 0 rgba(255, 255, 255, 0.15),
737
- var(--shadow-glow-primary),
738
- 0 0 60px rgba(129, 140, 248, 0.15);
739
- }
740
- 100% {
741
- box-shadow:
742
- var(--shadow-strong),
743
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
744
- var(--shadow-glow-primary),
745
- 0 0 80px rgba(129, 140, 248, 0.25),
746
- 0 0 120px rgba(34, 211, 238, 0.15);
747
- }
748
- }
749
-
750
- .topbar::before {
751
- content: '';
752
- position: absolute;
753
- top: 0;
754
- left: 0;
755
- right: 0;
756
- height: 3px;
757
- background: linear-gradient(90deg,
758
- transparent,
759
- var(--secondary) 20%,
760
- var(--primary) 50%,
761
- var(--secondary) 80%,
762
- transparent);
763
- opacity: 0.8;
764
- animation: headerShine 3s linear infinite;
765
- }
766
-
767
- @keyframes headerShine {
768
- 0% {
769
- transform: translateX(-100%);
770
- opacity: 0;
771
- }
772
- 50% {
773
- opacity: 1;
774
- }
775
- 100% {
776
- transform: translateX(100%);
777
- opacity: 0;
778
- }
779
- }
780
-
781
- .topbar::after {
782
- content: '';
783
- position: absolute;
784
- top: -50%;
785
- left: -50%;
786
- width: 200%;
787
- height: 200%;
788
- background: radial-gradient(circle, rgba(129, 140, 248, 0.1) 0%, transparent 70%);
789
- animation: headerPulse 6s ease-in-out infinite;
790
- pointer-events: none;
791
- }
792
-
793
- @keyframes headerPulse {
794
- 0%, 100% {
795
- transform: scale(1);
796
- opacity: 0.3;
797
- }
798
- 50% {
799
- transform: scale(1.1);
800
- opacity: 0.5;
801
- }
802
- }
803
-
804
- .topbar-content {
805
- display: flex;
806
- align-items: center;
807
- gap: 16px;
808
- flex: 1;
809
- }
810
-
811
- .topbar-icon {
812
- display: flex;
813
- align-items: center;
814
- justify-content: center;
815
- width: 80px;
816
- height: 80px;
817
- border-radius: 50%;
818
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.2) 0%, rgba(34, 211, 238, 0.15) 100%);
819
- border: 2px solid rgba(129, 140, 248, 0.3);
820
- color: var(--primary-light);
821
- flex-shrink: 0;
822
- box-shadow:
823
- inset 0 2px 4px rgba(255, 255, 255, 0.2),
824
- inset 0 -2px 4px rgba(0, 0, 0, 0.3),
825
- 0 6px 20px rgba(0, 0, 0, 0.4),
826
- 0 0 40px rgba(129, 140, 248, 0.3),
827
- 0 0 60px rgba(34, 211, 238, 0.2);
828
- position: relative;
829
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
830
- animation: iconFloat 3s ease-in-out infinite;
831
- backdrop-filter: blur(20px) saturate(180%);
832
- }
833
-
834
- @keyframes iconFloat {
835
- 0%, 100% {
836
- transform: translateY(0);
837
- }
838
- 50% {
839
- transform: translateY(-3px);
840
- }
841
- }
842
-
843
- .topbar-icon:hover {
844
- box-shadow:
845
- inset 0 1px 2px rgba(255, 255, 255, 0.2),
846
- inset 0 -1px 2px rgba(0, 0, 0, 0.3),
847
- 0 6px 16px rgba(0, 0, 0, 0.4),
848
- 0 0 30px rgba(129, 140, 248, 0.3);
849
- border-color: var(--primary);
850
- }
851
-
852
- .topbar-icon::before {
853
- content: '';
854
- position: absolute;
855
- top: 2px;
856
- left: 2px;
857
- right: 2px;
858
- height: 50%;
859
- border-radius: 14px 14px 0 0;
860
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent);
861
- pointer-events: none;
862
- }
863
-
864
- .topbar-icon svg {
865
- position: relative;
866
- z-index: 1;
867
- width: 36px;
868
- height: 36px;
869
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
870
- }
871
-
872
- .topbar-text {
873
- display: flex;
874
- flex-direction: column;
875
- gap: 6px;
876
- }
877
-
878
- .topbar h1 {
879
- margin: 0;
880
- font-size: 2.25rem;
881
- font-weight: 900;
882
- font-family: 'Manrope', 'DM Sans', sans-serif;
883
- letter-spacing: -0.04em;
884
- line-height: 1.2;
885
- display: flex;
886
- align-items: baseline;
887
- gap: 12px;
888
- position: relative;
889
- z-index: 1;
890
- filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
891
- }
892
-
893
- .title-gradient {
894
- background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 80%, var(--text-soft) 100%);
895
- -webkit-background-clip: text;
896
- -webkit-text-fill-color: transparent;
897
- background-clip: text;
898
- text-shadow: 0 0 40px rgba(255, 255, 255, 0.2);
899
- position: relative;
900
- animation: titleShimmer 3s ease-in-out infinite;
901
- }
902
-
903
- @keyframes titleShimmer {
904
- 0%, 100% {
905
- filter: brightness(1);
906
- }
907
- 50% {
908
- filter: brightness(1.2);
909
- }
910
- }
911
-
912
- .title-accent {
913
- background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
914
- -webkit-background-clip: text;
915
- -webkit-text-fill-color: transparent;
916
- background-clip: text;
917
- font-size: 0.85em;
918
- position: relative;
919
- animation: accentPulse 2s ease-in-out infinite;
920
- }
921
-
922
- @keyframes accentPulse {
923
- 0%, 100% {
924
- opacity: 1;
925
- filter: drop-shadow(0 0 8px rgba(129, 140, 248, 0.4));
926
- }
927
- 50% {
928
- opacity: 0.9;
929
- filter: drop-shadow(0 0 12px rgba(129, 140, 248, 0.6));
930
- }
931
- }
932
-
933
- .topbar p.text-muted {
934
- margin: 0;
935
- font-size: 0.875rem;
936
- color: var(--text-muted);
937
- font-weight: 500;
938
- font-family: 'Manrope', sans-serif;
939
- display: flex;
940
- align-items: center;
941
- gap: 4px;
942
- }
943
-
944
- .topbar p.text-muted svg {
945
- opacity: 0.7;
946
- color: var(--primary);
947
- }
948
-
949
- .status-group {
950
- display: flex;
951
- gap: 12px;
952
- flex-wrap: wrap;
953
- }
954
-
955
- .status-pill {
956
- display: flex;
957
- align-items: center;
958
- gap: 10px;
959
- padding: 10px 18px;
960
- border-radius: 14px;
961
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.15), rgba(34, 211, 238, 0.1));
962
- border: 2px solid rgba(129, 140, 248, 0.3);
963
- font-size: 0.8125rem;
964
- font-weight: 700;
965
- text-transform: uppercase;
966
- letter-spacing: 0.08em;
967
- font-family: 'Manrope', sans-serif;
968
- color: rgba(226, 232, 240, 0.95);
969
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
970
- position: relative;
971
- overflow: visible;
972
- box-shadow:
973
- 0 4px 12px rgba(0, 0, 0, 0.3),
974
- inset 0 1px 0 rgba(255, 255, 255, 0.15);
975
- cursor: pointer;
976
- backdrop-filter: blur(15px) saturate(180%);
977
- }
978
-
979
- .status-pill:hover {
980
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.25), rgba(34, 211, 238, 0.2));
981
- box-shadow:
982
- 0 6px 16px rgba(0, 0, 0, 0.4),
983
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
984
- 0 0 20px rgba(129, 140, 248, 0.3);
985
- border-color: rgba(129, 140, 248, 0.5);
986
- color: #ffffff;
987
- transform: translateY(-1px);
988
- }
989
-
990
- .status-pill::before {
991
- content: '';
992
- position: absolute;
993
- inset: 0;
994
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
995
- opacity: 0;
996
- transition: opacity 0.3s ease;
997
- }
998
-
999
- .status-pill:hover::before {
1000
- opacity: 1;
1001
- }
1002
-
1003
- .status-dot {
1004
- width: 10px;
1005
- height: 10px;
1006
- border-radius: 50%;
1007
- background: #FBBF24;
1008
- position: relative;
1009
- flex-shrink: 0;
1010
- box-shadow:
1011
- 0 0 12px #FBBF24,
1012
- 0 0 20px rgba(251, 191, 36, 0.5),
1013
- 0 2px 4px rgba(0, 0, 0, 0.3);
1014
- animation: pulse-dot 2s ease-in-out infinite;
1015
- border: 1px solid rgba(255, 255, 255, 0.3);
1016
- }
1017
-
1018
- @keyframes pulse-dot {
1019
- 0%, 100% {
1020
- opacity: 1;
1021
- transform: scale(1);
1022
- box-shadow: 0 0 12px #FBBF24, 0 0 20px rgba(251, 191, 36, 0.5);
1023
- }
1024
- 50% {
1025
- opacity: 0.8;
1026
- transform: scale(1.1);
1027
- box-shadow: 0 0 16px #FBBF24, 0 0 30px rgba(251, 191, 36, 0.7);
1028
- }
1029
- }
1030
-
1031
- .status-pill[data-state="ok"] .status-dot {
1032
- background: #34D399;
1033
- box-shadow:
1034
- 0 0 12px #34D399,
1035
- 0 0 20px rgba(52, 211, 153, 0.5),
1036
- 0 2px 4px rgba(0, 0, 0, 0.3);
1037
- animation: none;
1038
- }
1039
-
1040
- .status-pill[data-state="error"] .status-dot {
1041
- background: #F87171;
1042
- box-shadow:
1043
- 0 0 12px #F87171,
1044
- 0 0 20px rgba(248, 113, 113, 0.5),
1045
- 0 2px 4px rgba(0, 0, 0, 0.3);
1046
- animation: none;
1047
- }
1048
-
1049
- .status-pill .status-icon {
1050
- width: 16px;
1051
- height: 16px;
1052
- flex-shrink: 0;
1053
- color: rgba(226, 232, 240, 0.9);
1054
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
1055
- transition: all 0.3s ease;
1056
- }
1057
-
1058
- .status-pill:hover .status-icon {
1059
- color: #ffffff;
1060
- filter: drop-shadow(0 2px 4px rgba(129, 140, 248, 0.6));
1061
- }
1062
-
1063
- .status-pill[data-state="ok"] .status-icon {
1064
- color: #34D399;
1065
- }
1066
-
1067
- .status-pill[data-state="error"] .status-icon {
1068
- color: #F87171;
1069
- }
1070
-
1071
- .status-pill:hover .status-dot {
1072
- box-shadow:
1073
- 0 0 12px var(--warning),
1074
- 0 2px 6px rgba(0, 0, 0, 0.25);
1075
- }
1076
-
1077
- .status-pill[data-state='ok'] {
1078
- background: linear-gradient(135deg, var(--success) 0%, var(--success-dark) 100%);
1079
- border: 1px solid var(--success);
1080
- color: #ffffff;
1081
- box-shadow:
1082
- 0 2px 8px var(--success-glow),
1083
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
1084
- font-weight: 700;
1085
- position: relative;
1086
- }
1087
-
1088
- .status-pill[data-state='ok']:hover {
1089
- background: linear-gradient(135deg, var(--success-dark) 0%, #047857 100%);
1090
- box-shadow:
1091
- 0 4px 12px var(--success-glow),
1092
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
1093
- }
1094
-
1095
- @keyframes live-pulse {
1096
- 0%, 100% {
1097
- box-shadow:
1098
- inset 0 1px 2px rgba(255, 255, 255, 0.2),
1099
- inset 0 -1px 2px rgba(0, 0, 0, 0.3),
1100
- 0 4px 16px rgba(34, 197, 94, 0.4),
1101
- 0 0 30px rgba(34, 197, 94, 0.3),
1102
- 0 0 50px rgba(16, 185, 129, 0.2);
1103
- }
1104
- 50% {
1105
- box-shadow:
1106
- inset 0 1px 2px rgba(255, 255, 255, 0.25),
1107
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
1108
- 0 6px 24px rgba(34, 197, 94, 0.5),
1109
- 0 0 40px rgba(34, 197, 94, 0.4),
1110
- 0 0 60px rgba(16, 185, 129, 0.3);
1111
- }
1112
- }
1113
-
1114
- .status-pill[data-state='ok']::before {
1115
- content: '';
1116
- position: absolute;
1117
- top: 2px;
1118
- left: 2px;
1119
- right: 2px;
1120
- height: 50%;
1121
- border-radius: 999px 999px 0 0;
1122
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
1123
- pointer-events: none;
1124
- }
1125
-
1126
- .status-pill[data-state='ok'] .status-dot {
1127
- background: #ffffff;
1128
- border: 2px solid #10b981;
1129
- box-shadow:
1130
- 0 0 8px rgba(16, 185, 129, 0.6),
1131
- 0 2px 4px rgba(0, 0, 0, 0.2),
1132
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
1133
- }
1134
-
1135
- .status-pill[data-state='ok']:hover .status-dot {
1136
- box-shadow:
1137
- 0 0 12px rgba(16, 185, 129, 0.8),
1138
- 0 2px 6px rgba(0, 0, 0, 0.25),
1139
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
1140
- }
1141
-
1142
- @keyframes live-dot-pulse {
1143
- 0%, 100% {
1144
- transform: scale(1);
1145
- box-shadow:
1146
- inset 0 1px 2px rgba(255, 255, 255, 0.4),
1147
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
1148
- 0 0 16px rgba(34, 197, 94, 0.8),
1149
- 0 0 32px rgba(34, 197, 94, 0.6),
1150
- 0 0 48px rgba(16, 185, 129, 0.4);
1151
- }
1152
- 50% {
1153
- transform: scale(1.15);
1154
- box-shadow:
1155
- inset 0 1px 2px rgba(255, 255, 255, 0.5),
1156
- inset 0 -1px 2px rgba(0, 0, 0, 0.5),
1157
- 0 0 20px rgba(34, 197, 94, 1),
1158
- 0 0 40px rgba(34, 197, 94, 0.8),
1159
- 0 0 60px rgba(16, 185, 129, 0.6);
1160
- }
1161
- }
1162
-
1163
- .status-pill[data-state='ok']::after {
1164
- display: none;
1165
- }
1166
-
1167
- .status-pill[data-state='warn'] {
1168
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
1169
- border: 2px solid #f59e0b;
1170
- color: #ffffff;
1171
- box-shadow:
1172
- 0 2px 8px rgba(245, 158, 11, 0.3),
1173
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
1174
- }
1175
-
1176
- .status-pill[data-state='warn']:hover {
1177
- background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
1178
- box-shadow:
1179
- 0 4px 12px rgba(245, 158, 11, 0.4),
1180
- inset 0 1px 0 rgba(255, 255, 255, 0.4);
1181
- }
1182
-
1183
- .status-pill[data-state='warn'] .status-dot {
1184
- background: #ffffff;
1185
- border: 2px solid #f59e0b;
1186
- box-shadow:
1187
- 0 0 8px rgba(245, 158, 11, 0.6),
1188
- 0 2px 4px rgba(0, 0, 0, 0.2),
1189
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
1190
- }
1191
-
1192
- .status-pill[data-state='warn']:hover .status-dot {
1193
- box-shadow:
1194
- 0 0 12px rgba(245, 158, 11, 0.8),
1195
- 0 2px 6px rgba(0, 0, 0, 0.25),
1196
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
1197
- }
1198
-
1199
- .status-pill[data-state='error'] {
1200
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
1201
- border: 2px solid #ef4444;
1202
- color: #ffffff;
1203
- box-shadow:
1204
- 0 2px 8px rgba(239, 68, 68, 0.3),
1205
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
1206
- }
1207
-
1208
- .status-pill[data-state='error']:hover {
1209
- background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
1210
- box-shadow:
1211
- 0 4px 12px rgba(239, 68, 68, 0.4),
1212
- inset 0 1px 0 rgba(255, 255, 255, 0.4);
1213
- }
1214
-
1215
- .status-pill[data-state='error'] .status-dot {
1216
- background: #ffffff;
1217
- border: 2px solid #ef4444;
1218
- box-shadow:
1219
- 0 0 8px rgba(239, 68, 68, 0.6),
1220
- 0 2px 4px rgba(0, 0, 0, 0.2),
1221
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
1222
- }
1223
-
1224
- .status-pill[data-state='error']:hover .status-dot {
1225
- box-shadow:
1226
- 0 0 12px rgba(239, 68, 68, 0.8),
1227
- 0 2px 6px rgba(0, 0, 0, 0.25),
1228
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
1229
- }
1230
-
1231
- @keyframes pulse-green {
1232
- 0%, 100% {
1233
- transform: scale(1);
1234
- opacity: 1;
1235
- box-shadow:
1236
- 0 0 16px #86efac,
1237
- 0 0 32px rgba(74, 222, 128, 0.8),
1238
- 0 0 48px rgba(34, 197, 94, 0.6);
1239
- }
1240
- 50% {
1241
- transform: scale(1.3);
1242
- opacity: 0.9;
1243
- box-shadow:
1244
- 0 0 24px #86efac,
1245
- 0 0 48px rgba(74, 222, 128, 1),
1246
- 0 0 72px rgba(34, 197, 94, 0.8);
1247
- }
1248
- }
1249
-
1250
- @keyframes glow-pulse {
1251
- 0%, 100% {
1252
- opacity: 0.6;
1253
- transform: scale(1);
1254
- }
1255
- 50% {
1256
- opacity: 1;
1257
- transform: scale(1.1);
1258
- }
1259
- }
1260
-
1261
- .page-container {
1262
- flex: 1;
1263
- }
1264
-
1265
- .page {
1266
- display: none;
1267
- animation: fadeIn 0.6s ease;
1268
- }
1269
-
1270
- .page.active {
1271
- display: block;
1272
- }
1273
-
1274
- .section-header {
1275
- display: flex;
1276
- justify-content: space-between;
1277
- align-items: center;
1278
- margin-bottom: 24px;
1279
- padding-bottom: 16px;
1280
- border-bottom: 2px solid var(--glass-border);
1281
- position: relative;
1282
- }
1283
-
1284
- .section-header::after {
1285
- content: '';
1286
- position: absolute;
1287
- bottom: -2px;
1288
- left: 0;
1289
- width: 60px;
1290
- height: 2px;
1291
- background: linear-gradient(90deg, var(--primary), var(--secondary));
1292
- border-radius: 2px;
1293
- }
1294
-
1295
- .section-title {
1296
- font-size: 2rem;
1297
- font-weight: 900;
1298
- letter-spacing: -0.03em;
1299
- font-family: 'Manrope', 'DM Sans', sans-serif;
1300
- margin: 0;
1301
- background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
1302
- -webkit-background-clip: text;
1303
- -webkit-text-fill-color: transparent;
1304
- background-clip: text;
1305
- position: relative;
1306
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
1307
- }
1308
-
1309
- .glass-card {
1310
- background: var(--glass-bg);
1311
- backdrop-filter: blur(35px) saturate(180%);
1312
- -webkit-backdrop-filter: blur(35px) saturate(180%);
1313
- border: 1px solid var(--glass-border);
1314
- border-radius: 20px;
1315
- padding: 28px;
1316
- box-shadow:
1317
- 0 8px 32px rgba(0, 0, 0, 0.5),
1318
- inset 0 1px 0 rgba(255, 255, 255, 0.1),
1319
- inset 0 -1px 0 rgba(0, 0, 0, 0.2),
1320
- var(--shadow-glow-primary);
1321
- position: relative;
1322
- overflow: visible;
1323
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1324
- }
1325
-
1326
- .glass-card::before {
1327
- content: '';
1328
- position: absolute;
1329
- inset: -4px;
1330
- background: linear-gradient(135deg,
1331
- var(--secondary-glow) 0%,
1332
- var(--primary-glow) 50%,
1333
- rgba(244, 114, 182, 0.3) 100%);
1334
- border-radius: 24px;
1335
- opacity: 0;
1336
- transition: opacity 0.4s ease;
1337
- z-index: -1;
1338
- filter: blur(20px);
1339
- animation: card-glow-pulse 4s ease-in-out infinite;
1340
- }
1341
-
1342
- @keyframes card-glow-pulse {
1343
- 0%, 100% {
1344
- opacity: 0;
1345
- filter: blur(20px);
1346
- }
1347
- 50% {
1348
- opacity: 0.4;
1349
- filter: blur(25px);
1350
- }
1351
- }
1352
-
1353
- .glass-card::after {
1354
- content: '';
1355
- position: absolute;
1356
- top: 0;
1357
- left: 0;
1358
- right: 0;
1359
- height: 3px;
1360
- background: linear-gradient(90deg,
1361
- transparent,
1362
- var(--secondary),
1363
- var(--primary),
1364
- var(--accent),
1365
- transparent);
1366
- border-radius: 20px 20px 0 0;
1367
- opacity: 0.7;
1368
- animation: card-shimmer 4s infinite;
1369
- }
1370
-
1371
- @keyframes card-shimmer {
1372
- 0%, 100% { opacity: 0.7; }
1373
- 50% { opacity: 1; }
1374
- }
1375
-
1376
-
1377
- .glass-card:hover {
1378
- background: var(--glass-bg-strong);
1379
- box-shadow:
1380
- 0 16px 48px rgba(0, 0, 0, 0.6),
1381
- var(--shadow-glow-primary),
1382
- var(--shadow-glow-secondary),
1383
- inset 0 1px 0 rgba(255, 255, 255, 0.15),
1384
- inset 0 -1px 0 rgba(0, 0, 0, 0.3);
1385
- border-color: var(--glass-border-strong);
1386
- }
1387
-
1388
- .glass-card:hover::before {
1389
- opacity: 0.8;
1390
- filter: blur(30px);
1391
- }
1392
-
1393
- .glass-card:hover::after {
1394
- opacity: 1;
1395
- height: 4px;
1396
- }
1397
-
1398
- .card-header {
1399
- display: flex;
1400
- justify-content: space-between;
1401
- align-items: center;
1402
- margin-bottom: 20px;
1403
- padding-bottom: 16px;
1404
- border-bottom: 1px solid var(--glass-border);
1405
- }
1406
-
1407
- .card-header h4 {
1408
- margin: 0;
1409
- font-size: 1.25rem;
1410
- font-weight: 700;
1411
- font-family: 'Manrope', 'DM Sans', sans-serif;
1412
- color: var(--text-primary);
1413
- letter-spacing: -0.02em;
1414
- }
1415
-
1416
- .glass-card h4 {
1417
- font-size: 1.25rem;
1418
- font-weight: 700;
1419
- font-family: 'Manrope', 'DM Sans', sans-serif;
1420
- margin: 0 0 20px 0;
1421
- color: var(--text-primary);
1422
- letter-spacing: -0.02em;
1423
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
1424
- -webkit-background-clip: text;
1425
- -webkit-text-fill-color: transparent;
1426
- background-clip: text;
1427
- }
1428
-
1429
- .glass-card::before {
1430
- content: '';
1431
- position: absolute;
1432
- inset: 0;
1433
- background: linear-gradient(120deg, transparent, var(--glass-highlight), transparent);
1434
- opacity: 0;
1435
- transition: opacity 0.4s ease;
1436
- }
1437
-
1438
- .glass-card:hover::before {
1439
- opacity: 1;
1440
- }
1441
-
1442
- .stats-grid {
1443
- display: grid;
1444
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1445
- gap: 18px;
1446
- margin-bottom: 24px;
1447
- }
1448
-
1449
- .stat-card {
1450
- display: flex;
1451
- flex-direction: column;
1452
- gap: 18px;
1453
- position: relative;
1454
- background: linear-gradient(135deg, rgba(129, 140, 248, 0.15), rgba(34, 211, 238, 0.1));
1455
- padding: 28px;
1456
- border-radius: 20px;
1457
- border: 2px solid rgba(129, 140, 248, 0.25);
1458
- backdrop-filter: blur(30px) saturate(180%);
1459
- -webkit-backdrop-filter: blur(30px) saturate(180%);
1460
- box-shadow:
1461
- 0 8px 32px rgba(0, 0, 0, 0.4),
1462
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
1463
- inset 0 -1px 0 rgba(0, 0, 0, 0.2);
1464
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1465
- overflow: visible;
1466
- }
1467
-
1468
- .stat-card::before {
1469
- content: '';
1470
- position: absolute;
1471
- inset: -2px;
1472
- border-radius: 22px;
1473
- background: linear-gradient(135deg,
1474
- rgba(129, 140, 248, 0.2) 0%,
1475
- rgba(34, 211, 238, 0.15) 100%);
1476
- opacity: 0;
1477
- transition: opacity 0.3s ease;
1478
- z-index: -1;
1479
- filter: blur(8px);
1480
- }
1481
-
1482
- .stat-card::after {
1483
- content: '';
1484
- position: absolute;
1485
- top: 0;
1486
- left: 0;
1487
- right: 0;
1488
- height: 2px;
1489
- background: linear-gradient(90deg,
1490
- transparent,
1491
- rgba(129, 140, 248, 0.6),
1492
- rgba(34, 211, 238, 0.6),
1493
- transparent);
1494
- border-radius: 20px 20px 0 0;
1495
- opacity: 0.5;
1496
- transition: opacity 0.3s ease;
1497
- }
1498
-
1499
- .stat-card:hover {
1500
- border-color: rgba(0, 212, 255, 0.5);
1501
- box-shadow:
1502
- 0 16px 48px rgba(0, 0, 0, 0.5),
1503
- 0 0 40px rgba(0, 212, 255, 0.4),
1504
- 0 0 80px rgba(139, 92, 246, 0.3),
1505
- inset 0 1px 0 rgba(255, 255, 255, 0.3),
1506
- inset 0 -1px 0 rgba(0, 0, 0, 0.3);
1507
- }
1508
-
1509
- .stat-card:hover::before {
1510
- opacity: 0.4;
1511
- filter: blur(10px);
1512
- }
1513
-
1514
- .stat-card:hover::after {
1515
- opacity: 0.8;
1516
- height: 2px;
1517
- }
1518
-
1519
- .stat-header {
1520
- display: flex;
1521
- align-items: center;
1522
- gap: 0.75rem;
1523
- }
1524
-
1525
- .stat-icon {
1526
- display: flex;
1527
- align-items: center;
1528
- justify-content: center;
1529
- width: 52px;
1530
- height: 52px;
1531
- border-radius: 14px;
1532
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(139, 92, 246, 0.2));
1533
- flex-shrink: 0;
1534
- border: 2px solid rgba(0, 212, 255, 0.3);
1535
- box-shadow:
1536
- inset 0 1px 2px rgba(255, 255, 255, 0.2),
1537
- inset 0 -1px 2px rgba(0, 0, 0, 0.3),
1538
- 0 4px 12px rgba(0, 212, 255, 0.3),
1539
- 0 0 20px rgba(0, 212, 255, 0.2);
1540
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1541
- position: relative;
1542
- color: #00D4FF;
1543
- overflow: visible;
1544
- }
1545
-
1546
- .stat-icon::after {
1547
- content: '';
1548
- position: absolute;
1549
- inset: -2px;
1550
- border-radius: 16px;
1551
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.4), rgba(139, 92, 246, 0.4));
1552
- opacity: 0;
1553
- filter: blur(12px);
1554
- transition: opacity 0.4s ease;
1555
- z-index: -1;
1556
- }
1557
-
1558
- .stat-icon::before {
1559
- content: '';
1560
- position: absolute;
1561
- top: 2px;
1562
- left: 2px;
1563
- right: 2px;
1564
- height: 50%;
1565
- border-radius: 12px 12px 0 0;
1566
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.15), transparent);
1567
- pointer-events: none;
1568
- }
1569
-
1570
- .stat-icon svg {
1571
- position: relative;
1572
- z-index: 1;
1573
- width: 30px;
1574
- height: 30px;
1575
- opacity: 1;
1576
- filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
1577
- stroke-width: 2.5;
1578
- }
1579
-
1580
- .stat-card:hover .stat-icon {
1581
- box-shadow:
1582
- inset 0 1px 2px rgba(255, 255, 255, 0.25),
1583
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
1584
- 0 8px 24px rgba(0, 212, 255, 0.5),
1585
- 0 0 40px rgba(0, 212, 255, 0.4),
1586
- 0 0 60px rgba(139, 92, 246, 0.3);
1587
- border-color: rgba(0, 212, 255, 0.6);
1588
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), rgba(139, 92, 246, 0.3));
1589
- }
1590
-
1591
- .stat-card:hover .stat-icon::after {
1592
- opacity: 0.8;
1593
- filter: blur(16px);
1594
- }
1595
-
1596
- .stat-card:hover .stat-icon svg {
1597
- opacity: 1;
1598
- }
1599
-
1600
- .stat-card h3 {
1601
- font-size: 0.8125rem;
1602
- font-weight: 700;
1603
- text-transform: uppercase;
1604
- letter-spacing: 0.1em;
1605
- color: rgba(255, 255, 255, 0.7);
1606
- margin: 0;
1607
- font-family: 'Manrope', 'DM Sans', sans-serif;
1608
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1609
- }
1610
-
1611
- .stat-label {
1612
- font-size: 0.8125rem;
1613
- font-weight: 700;
1614
- text-transform: uppercase;
1615
- letter-spacing: 0.1em;
1616
- color: rgba(226, 232, 240, 0.95);
1617
- margin: 0;
1618
- font-family: 'Manrope', 'DM Sans', sans-serif;
1619
- text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
1620
- line-height: 1.5;
1621
- }
1622
-
1623
- .stat-value {
1624
- font-size: 2.75rem;
1625
- font-weight: 900;
1626
- margin: 0;
1627
- font-family: 'Manrope', 'DM Sans', sans-serif;
1628
- letter-spacing: -0.05em;
1629
- line-height: 1.2;
1630
- color: #ffffff;
1631
- text-shadow:
1632
- 0 2px 8px rgba(0, 0, 0, 0.6),
1633
- 0 0 20px rgba(129, 140, 248, 0.4),
1634
- 0 0 40px rgba(34, 211, 238, 0.3);
1635
- position: relative;
1636
- }
1637
-
1638
- .stat-card:hover .stat-value {
1639
- text-shadow:
1640
- 0 2px 10px rgba(0, 0, 0, 0.7),
1641
- 0 0 30px rgba(129, 140, 248, 0.6),
1642
- 0 0 50px rgba(34, 211, 238, 0.5);
1643
- transform: scale(1.02);
1644
- }
1645
-
1646
- .stat-value-wrapper {
1647
- display: flex;
1648
- flex-direction: column;
1649
- gap: 0.5rem;
1650
- margin: 0.5rem 0;
1651
- }
1652
-
1653
- .stat-change {
1654
- display: inline-flex;
1655
- align-items: center;
1656
- gap: 0.375rem;
1657
- font-size: 0.8125rem;
1658
- font-weight: 600;
1659
- font-family: 'Manrope', sans-serif;
1660
- width: fit-content;
1661
- transition: all 0.2s ease;
1662
- }
1663
-
1664
- .change-icon-wrapper {
1665
- display: flex;
1666
- align-items: center;
1667
- justify-content: center;
1668
- width: 16px;
1669
- height: 16px;
1670
- flex-shrink: 0;
1671
- opacity: 0.8;
1672
- }
1673
-
1674
- .change-icon-wrapper.positive {
1675
- color: #22c55e;
1676
- }
1677
-
1678
- .change-icon-wrapper.negative {
1679
- color: #ef4444;
1680
- }
1681
-
1682
- .stat-change.positive {
1683
- color: #4ade80;
1684
- background: rgba(34, 197, 94, 0.2);
1685
- padding: 4px 10px;
1686
- border-radius: 8px;
1687
- border: 1px solid rgba(34, 197, 94, 0.4);
1688
- box-shadow:
1689
- 0 2px 8px rgba(34, 197, 94, 0.3),
1690
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
1691
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1692
- font-weight: 700;
1693
- }
1694
-
1695
- .stat-change.negative {
1696
- color: #f87171;
1697
- background: rgba(239, 68, 68, 0.2);
1698
- padding: 4px 10px;
1699
- border-radius: 8px;
1700
- border: 1px solid rgba(239, 68, 68, 0.4);
1701
- box-shadow:
1702
- 0 2px 8px rgba(239, 68, 68, 0.3),
1703
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
1704
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1705
- font-weight: 700;
1706
- }
1707
-
1708
- .change-value {
1709
- font-weight: 600;
1710
- letter-spacing: 0.01em;
1711
- }
1712
-
1713
- .stat-metrics {
1714
- display: flex;
1715
- gap: 1rem;
1716
- margin-top: auto;
1717
- padding-top: 1rem;
1718
- border-top: 2px solid rgba(255, 255, 255, 0.12);
1719
- background: linear-gradient(90deg,
1720
- transparent,
1721
- rgba(0, 212, 255, 0.05),
1722
- rgba(139, 92, 246, 0.05),
1723
- transparent);
1724
- margin-left: -20px;
1725
- margin-right: -20px;
1726
- padding-left: 20px;
1727
- padding-right: 20px;
1728
- border-radius: 0 0 20px 20px;
1729
- }
1730
-
1731
- .stat-metric {
1732
- display: flex;
1733
- flex-direction: column;
1734
- gap: 0.25rem;
1735
- flex: 1;
1736
- }
1737
-
1738
- .stat-metric .metric-label {
1739
- font-size: 0.7rem;
1740
- text-transform: uppercase;
1741
- letter-spacing: 0.1em;
1742
- color: rgba(255, 255, 255, 0.6);
1743
- font-weight: 700;
1744
- font-family: 'Manrope', sans-serif;
1745
- text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
1746
- }
1747
-
1748
- .stat-metric .metric-value {
1749
- font-size: 0.9375rem;
1750
- font-weight: 700;
1751
- font-family: 'Manrope', 'DM Sans', sans-serif;
1752
- color: rgba(255, 255, 255, 0.9);
1753
- display: flex;
1754
- align-items: center;
1755
- gap: 6px;
1756
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
1757
- }
1758
-
1759
- .metric-icon {
1760
- display: inline-flex;
1761
- align-items: center;
1762
- justify-content: center;
1763
- width: 18px;
1764
- height: 18px;
1765
- border-radius: 4px;
1766
- font-size: 0.75rem;
1767
- font-weight: 700;
1768
- flex-shrink: 0;
1769
- }
1770
-
1771
- .metric-icon.positive {
1772
- background: rgba(34, 197, 94, 0.3);
1773
- color: #4ade80;
1774
- border: 1px solid rgba(34, 197, 94, 0.5);
1775
- box-shadow:
1776
- 0 2px 8px rgba(34, 197, 94, 0.4),
1777
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
1778
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1779
- }
1780
-
1781
- .metric-icon.negative {
1782
- background: rgba(239, 68, 68, 0.3);
1783
- color: #f87171;
1784
- border: 1px solid rgba(239, 68, 68, 0.5);
1785
- box-shadow:
1786
- 0 2px 8px rgba(239, 68, 68, 0.4),
1787
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
1788
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1789
- }
1790
-
1791
- .stat-metric .metric-value.positive {
1792
- color: #4ade80;
1793
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1794
- font-weight: 800;
1795
- }
1796
-
1797
- .stat-metric .metric-value.negative {
1798
- color: #f87171;
1799
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1800
- font-weight: 800;
1801
- }
1802
-
1803
- .stat-trend {
1804
- display: flex;
1805
- align-items: center;
1806
- gap: 6px;
1807
- font-size: 0.8125rem;
1808
- color: var(--text-faint);
1809
- font-family: 'Manrope', sans-serif;
1810
- font-weight: 500;
1811
- margin-top: auto;
1812
- letter-spacing: 0.02em;
1813
- }
1814
-
1815
- .grid-two {
1816
- display: grid;
1817
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
1818
- gap: 20px;
1819
- }
1820
-
1821
- .grid-three {
1822
- display: grid;
1823
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1824
- gap: 18px;
1825
- }
1826
-
1827
- .grid-four {
1828
- display: grid;
1829
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1830
- gap: 18px;
1831
- }
1832
-
1833
- .table-wrapper {
1834
- overflow: auto;
1835
- }
1836
-
1837
- table {
1838
- width: 100%;
1839
- border-collapse: separate;
1840
- border-spacing: 0;
1841
- }
1842
-
1843
- th, td {
1844
- text-align: left;
1845
- padding: 12px 14px;
1846
- font-size: 0.8125rem;
1847
- font-family: 'Manrope', 'DM Sans', sans-serif;
1848
- }
1849
-
1850
- th {
1851
- font-size: 0.7rem;
1852
- font-weight: 700;
1853
- letter-spacing: 0.06em;
1854
- color: var(--text-muted);
1855
- text-transform: uppercase;
1856
- border-bottom: 2px solid rgba(255, 255, 255, 0.1);
1857
- background: rgba(255, 255, 255, 0.03);
1858
- position: sticky;
1859
- top: 0;
1860
- z-index: 10;
1861
- white-space: nowrap;
1862
- }
1863
-
1864
- th:first-child {
1865
- border-top-left-radius: 12px;
1866
- padding-left: 16px;
1867
- }
1868
-
1869
- th:last-child {
1870
- border-top-right-radius: 12px;
1871
- padding-right: 16px;
1872
- }
1873
-
1874
- td {
1875
- font-weight: 500;
1876
- color: var(--text-primary);
1877
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1878
- vertical-align: middle;
1879
- }
1880
-
1881
- td:first-child {
1882
- padding-left: 16px;
1883
- font-weight: 600;
1884
- color: var(--text-muted);
1885
- font-size: 0.75rem;
1886
- }
1887
-
1888
- td:last-child {
1889
- padding-right: 16px;
1890
- }
1891
-
1892
- tr {
1893
- transition: all 0.2s ease;
1894
- }
1895
-
1896
- tbody tr {
1897
- border-left: 2px solid transparent;
1898
- transition: all 0.2s ease;
1899
- }
1900
-
1901
- tbody tr:hover {
1902
- background: rgba(255, 255, 255, 0.05);
1903
- border-left-color: rgba(143, 136, 255, 0.4);
1904
- transform: translateX(2px);
1905
- }
1906
-
1907
- tbody tr:last-child td:first-child {
1908
- border-bottom-left-radius: 12px;
1909
- }
1910
-
1911
- tbody tr:last-child td:last-child {
1912
- border-bottom-right-radius: 12px;
1913
- }
1914
-
1915
- tbody tr:last-child td {
1916
- border-bottom: none;
1917
- }
1918
-
1919
- td.text-success,
1920
- td.text-danger {
1921
- display: flex;
1922
- align-items: center;
1923
- gap: 6px;
1924
- font-weight: 600;
1925
- font-size: 0.8125rem;
1926
- }
1927
-
1928
- td.text-success {
1929
- color: #22c55e;
1930
- }
1931
-
1932
- td.text-danger {
1933
- color: #ef4444;
1934
- }
1935
-
1936
- .table-change-icon {
1937
- display: inline-flex;
1938
- align-items: center;
1939
- justify-content: center;
1940
- width: 16px;
1941
- height: 16px;
1942
- flex-shrink: 0;
1943
- opacity: 0.9;
1944
- }
1945
-
1946
- .table-change-icon.positive {
1947
- color: #22c55e;
1948
- }
1949
-
1950
- .table-change-icon.negative {
1951
- color: #ef4444;
1952
- }
1953
-
1954
- /* Chip styling for symbol column */
1955
- .chip {
1956
- display: inline-flex;
1957
- align-items: center;
1958
- padding: 6px 12px;
1959
- background: var(--glass-bg-light);
1960
- border: 1px solid var(--glass-border);
1961
- border-radius: 6px;
1962
- font-size: 0.75rem;
1963
- font-weight: 600;
1964
- color: var(--primary-light);
1965
- font-family: 'Manrope', sans-serif;
1966
- letter-spacing: 0.02em;
1967
- text-transform: uppercase;
1968
- }
1969
-
1970
- .badge {
1971
- padding: 4px 10px;
1972
- border-radius: 999px;
1973
- font-size: 0.75rem;
1974
- letter-spacing: 0.05em;
1975
- text-transform: uppercase;
1976
- }
1977
-
1978
- .badge-success { background: rgba(52, 211, 153, 0.2); color: var(--success-light); border: 1px solid var(--success); }
1979
- .badge-danger { background: rgba(248, 113, 113, 0.2); color: var(--danger-light); border: 1px solid var(--danger); }
1980
- .badge-cyan { background: rgba(34, 211, 238, 0.2); color: var(--secondary-light); border: 1px solid var(--secondary); }
1981
- .badge-neutral { background: var(--glass-bg-light); color: var(--text-muted); border: 1px solid var(--glass-border); }
1982
- .text-muted { color: var(--text-muted); }
1983
- .text-success { color: var(--success); }
1984
- .text-danger { color: var(--danger); }
1985
-
1986
- .ai-result {
1987
- margin-top: 20px;
1988
- padding: 24px;
1989
- border-radius: 20px;
1990
- border: 1px solid var(--glass-border);
1991
- background: var(--glass-bg);
1992
- backdrop-filter: blur(20px);
1993
- box-shadow: var(--shadow-soft);
1994
- }
1995
-
1996
- .action-badge {
1997
- display: inline-flex;
1998
- padding: 6px 14px;
1999
- border-radius: 999px;
2000
- letter-spacing: 0.08em;
2001
- font-weight: 600;
2002
- margin-bottom: 10px;
2003
- }
2004
-
2005
- .action-buy { background: rgba(52, 211, 153, 0.2); color: var(--success-light); border: 1px solid var(--success); }
2006
- .action-sell { background: rgba(248, 113, 113, 0.2); color: var(--danger-light); border: 1px solid var(--danger); }
2007
- .action-hold { background: rgba(96, 165, 250, 0.2); color: var(--info-light); border: 1px solid var(--info); }
2008
-
2009
- .ai-insights ul {
2010
- padding-left: 20px;
2011
- }
2012
-
2013
- .chip-row {
2014
- display: flex;
2015
- gap: 8px;
2016
- flex-wrap: wrap;
2017
- margin: 12px 0;
2018
- }
2019
-
2020
- .news-item {
2021
- padding: 12px 0;
2022
- border-bottom: 1px solid var(--glass-border);
2023
- }
2024
-
2025
- .ai-block {
2026
- padding: 14px;
2027
- border-radius: 12px;
2028
- border: 1px dashed var(--glass-border);
2029
- margin-top: 12px;
2030
- }
2031
-
2032
- .controls-bar {
2033
- display: flex;
2034
- flex-wrap: wrap;
2035
- gap: 12px;
2036
- margin-bottom: 16px;
2037
- }
2038
-
2039
- .input-chip {
2040
- border: 1px solid var(--glass-border);
2041
- background: rgba(255, 255, 255, 0.05);
2042
- border-radius: 999px;
2043
- padding: 8px 14px;
2044
- color: var(--text-muted);
2045
- display: inline-flex;
2046
- align-items: center;
2047
- gap: 10px;
2048
- font-family: 'Inter', sans-serif;
2049
- font-size: 0.875rem;
2050
- }
2051
-
2052
- .search-bar {
2053
- display: flex;
2054
- flex-wrap: wrap;
2055
- gap: 12px;
2056
- align-items: center;
2057
- margin-bottom: 20px;
2058
- padding: 16px;
2059
- background: var(--glass-bg);
2060
- border: 1px solid var(--glass-border);
2061
- border-radius: 16px;
2062
- backdrop-filter: blur(10px);
2063
- }
2064
-
2065
- .button-group {
2066
- display: flex;
2067
- gap: 8px;
2068
- flex-wrap: wrap;
2069
- }
2070
-
2071
- input[type='text'], select, textarea {
2072
- width: 100%;
2073
- background: rgba(255, 255, 255, 0.05);
2074
- border: 1px solid var(--glass-border);
2075
- border-radius: 12px;
2076
- padding: 12px 16px;
2077
- color: var(--text-primary);
2078
- font-family: 'Inter', sans-serif;
2079
- font-size: 0.9375rem;
2080
- transition: all 0.2s ease;
2081
- }
2082
-
2083
- input[type='text']:focus, select:focus, textarea:focus {
2084
- outline: none;
2085
- border-color: var(--primary);
2086
- background: rgba(255, 255, 255, 0.08);
2087
- box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
2088
- }
2089
-
2090
- textarea {
2091
- min-height: 100px;
2092
- }
2093
-
2094
- button.primary {
2095
- background: linear-gradient(120deg, var(--primary), var(--secondary));
2096
- border: none;
2097
- border-radius: 10px;
2098
- color: #fff;
2099
- padding: 10px 14px;
2100
- font-weight: 500;
2101
- font-family: 'Manrope', sans-serif;
2102
- font-size: 0.875rem;
2103
- cursor: pointer;
2104
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2105
- box-shadow:
2106
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
2107
- 0 2px 8px rgba(143, 136, 255, 0.2);
2108
- position: relative;
2109
- overflow: visible;
2110
- display: flex;
2111
- align-items: center;
2112
- gap: 10px;
2113
- }
2114
-
2115
- button.primary::before {
2116
- content: '';
2117
- position: absolute;
2118
- top: 0;
2119
- left: 0;
2120
- right: 0;
2121
- height: 50%;
2122
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
2123
- border-radius: 12px 12px 0 0;
2124
- pointer-events: none;
2125
- }
2126
-
2127
- button.primary:hover {
2128
- background: linear-gradient(135deg, #2563eb 0%, #4f46e5 50%, #7c3aed 100%);
2129
- box-shadow:
2130
- 0 6px 20px rgba(59, 130, 246, 0.5),
2131
- inset 0 1px 0 rgba(255, 255, 255, 0.4),
2132
- inset 0 -1px 0 rgba(0, 0, 0, 0.15);
2133
- }
2134
-
2135
- button.primary:hover::before {
2136
- height: 50%;
2137
- opacity: 1;
2138
- }
2139
-
2140
- button.primary:active {
2141
- box-shadow:
2142
- 0 2px 8px rgba(59, 130, 246, 0.4),
2143
- inset 0 2px 4px rgba(0, 0, 0, 0.2);
2144
- }
2145
-
2146
- button.secondary {
2147
- background: rgba(255, 255, 255, 0.95);
2148
- border: 2px solid #3b82f6;
2149
- border-radius: 12px;
2150
- color: #3b82f6;
2151
- padding: 14px 28px;
2152
- font-weight: 700;
2153
- font-family: 'Manrope', sans-serif;
2154
- font-size: 0.875rem;
2155
- cursor: pointer;
2156
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2157
- position: relative;
2158
- overflow: hidden;
2159
- display: flex;
2160
- align-items: center;
2161
- gap: 10px;
2162
- box-shadow:
2163
- 0 2px 8px rgba(59, 130, 246, 0.2),
2164
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
2165
- }
2166
-
2167
- button.secondary::before {
2168
- content: '';
2169
- position: absolute;
2170
- top: 0;
2171
- left: 0;
2172
- right: 0;
2173
- height: 50%;
2174
- background: linear-gradient(180deg, rgba(59, 130, 246, 0.1), transparent);
2175
- border-radius: 12px 12px 0 0;
2176
- pointer-events: none;
2177
- }
2178
-
2179
- button.secondary::before {
2180
- content: '';
2181
- position: absolute;
2182
- left: 0;
2183
- top: 50%;
2184
- transform: translateY(-50%);
2185
- width: 2px;
2186
- height: 0;
2187
- background: var(--primary);
2188
- border-radius: 0 2px 2px 0;
2189
- transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2190
- opacity: 0;
2191
- }
2192
-
2193
- button.secondary::after {
2194
- content: '';
2195
- position: absolute;
2196
- inset: 0;
2197
- background: rgba(143, 136, 255, 0.05);
2198
- border-radius: 10px;
2199
- opacity: 0;
2200
- transition: opacity 0.25s ease;
2201
- z-index: -1;
2202
- }
2203
-
2204
- button.secondary:hover {
2205
- background: #3b82f6;
2206
- color: #ffffff;
2207
- box-shadow:
2208
- 0 4px 16px rgba(59, 130, 246, 0.4),
2209
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
2210
- }
2211
-
2212
- button.secondary:hover::before {
2213
- height: 50%;
2214
- opacity: 1;
2215
- }
2216
-
2217
- button.secondary:hover::after {
2218
- opacity: 1;
2219
- }
2220
-
2221
- button.secondary.active {
2222
- background: rgba(143, 136, 255, 0.12);
2223
- border-color: rgba(143, 136, 255, 0.2);
2224
- color: var(--text-primary);
2225
- font-weight: 600;
2226
- box-shadow:
2227
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
2228
- 0 2px 8px rgba(143, 136, 255, 0.2);
2229
- }
2230
-
2231
- button.secondary.active::before {
2232
- height: 60%;
2233
- opacity: 1;
2234
- box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
2235
- }
2236
-
2237
- button.ghost {
2238
- background: rgba(255, 255, 255, 0.9);
2239
- border: 1px solid rgba(0, 0, 0, 0.1);
2240
- border-radius: 10px;
2241
- padding: 10px 16px;
2242
- color: #475569;
2243
- font-weight: 600;
2244
- font-family: 'Manrope', sans-serif;
2245
- font-size: 0.875rem;
2246
- cursor: pointer;
2247
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2248
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
2249
- position: relative;
2250
- overflow: visible;
2251
- display: flex;
2252
- align-items: center;
2253
- gap: 10px;
2254
- }
2255
-
2256
- button.ghost::before {
2257
- content: '';
2258
- position: absolute;
2259
- left: 0;
2260
- top: 50%;
2261
- transform: translateY(-50%);
2262
- width: 2px;
2263
- height: 0;
2264
- background: var(--primary);
2265
- border-radius: 0 2px 2px 0;
2266
- transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2267
- opacity: 0;
2268
- }
2269
-
2270
- button.ghost::after {
2271
- content: '';
2272
- position: absolute;
2273
- inset: 0;
2274
- background: rgba(143, 136, 255, 0.05);
2275
- border-radius: 10px;
2276
- opacity: 0;
2277
- transition: opacity 0.25s ease;
2278
- z-index: -1;
2279
- }
2280
-
2281
- button.ghost:hover {
2282
- background: rgba(255, 255, 255, 1);
2283
- border-color: rgba(59, 130, 246, 0.3);
2284
- color: #3b82f6;
2285
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
2286
- }
2287
-
2288
- button.ghost:hover::before {
2289
- height: 50%;
2290
- opacity: 1;
2291
- }
2292
-
2293
- button.ghost:hover::after {
2294
- opacity: 1;
2295
- }
2296
-
2297
- button.ghost.active {
2298
- background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(99, 102, 241, 0.12));
2299
- border-color: rgba(59, 130, 246, 0.4);
2300
- color: #3b82f6;
2301
- box-shadow:
2302
- inset 0 1px 2px rgba(255, 255, 255, 0.3),
2303
- 0 2px 8px rgba(59, 130, 246, 0.3);
2304
- }
2305
-
2306
- button.ghost.active::before {
2307
- height: 60%;
2308
- opacity: 1;
2309
- box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
2310
- }
2311
-
2312
- .skeleton {
2313
- position: relative;
2314
- overflow: hidden;
2315
- background: rgba(255, 255, 255, 0.05);
2316
- border-radius: 12px;
2317
- }
2318
-
2319
- .skeleton-block {
2320
- display: inline-block;
2321
- width: 100%;
2322
- height: 12px;
2323
- border-radius: 999px;
2324
- background: rgba(255, 255, 255, 0.08);
2325
- }
2326
-
2327
- .skeleton::after {
2328
- content: '';
2329
- position: absolute;
2330
- inset: 0;
2331
- transform: translateX(-100%);
2332
- background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.25), transparent);
2333
- animation: shimmer 1.5s infinite;
2334
- }
2335
-
2336
- .drawer {
2337
- position: fixed;
2338
- top: 0;
2339
- right: 0;
2340
- height: 100vh;
2341
- width: min(420px, 90vw);
2342
- background: rgba(5, 7, 12, 0.92);
2343
- border-left: 1px solid var(--glass-border);
2344
- transform: translateX(100%);
2345
- transition: transform 0.4s ease;
2346
- padding: 32px;
2347
- overflow-y: auto;
2348
- z-index: 40;
2349
- }
2350
-
2351
- .drawer.active {
2352
- transform: translateX(0);
2353
- }
2354
-
2355
- .modal-backdrop {
2356
- position: fixed;
2357
- inset: 0;
2358
- background: rgba(2, 6, 23, 0.75);
2359
- backdrop-filter: blur(8px);
2360
- display: none;
2361
- align-items: center;
2362
- justify-content: center;
2363
- z-index: 10000;
2364
- animation: fadeIn 0.3s ease;
2365
- }
2366
-
2367
- @keyframes fadeIn {
2368
- from {
2369
- opacity: 0;
2370
- }
2371
- to {
2372
- opacity: 1;
2373
- }
2374
- }
2375
-
2376
- .modal-backdrop.active {
2377
- display: flex;
2378
- }
2379
-
2380
- .modal {
2381
- width: min(640px, 90vw);
2382
- background: var(--glass-bg);
2383
- border-radius: 28px;
2384
- padding: 28px;
2385
- border: 1px solid var(--glass-border);
2386
- backdrop-filter: blur(20px);
2387
- }
2388
-
2389
- .inline-message {
2390
- border-radius: 16px;
2391
- padding: 16px 18px;
2392
- border: 1px solid var(--glass-border);
2393
- }
2394
-
2395
- .inline-error { border-color: rgba(239, 68, 68, 0.4); background: rgba(239, 68, 68, 0.08); }
2396
- .inline-warn { border-color: rgba(250, 204, 21, 0.4); background: rgba(250, 204, 21, 0.1); }
2397
- .inline-info { border-color: rgba(56, 189, 248, 0.4); background: rgba(56, 189, 248, 0.1); }
2398
-
2399
- .log-table {
2400
- font-family: 'JetBrains Mono', 'Space Grotesk', monospace;
2401
- font-size: 0.8rem;
2402
- }
2403
-
2404
- .chip {
2405
- padding: 6px 12px;
2406
- border-radius: 999px;
2407
- background: var(--glass-bg-light);
2408
- border: 1px solid var(--glass-border);
2409
- color: var(--text-secondary);
2410
- font-size: 0.75rem;
2411
- font-weight: 500;
2412
- }
2413
-
2414
- .backend-info-grid {
2415
- display: grid;
2416
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2417
- gap: 16px;
2418
- }
2419
-
2420
- .backend-info-item {
2421
- display: flex;
2422
- flex-direction: column;
2423
- gap: 8px;
2424
- padding: 16px;
2425
- background: rgba(255, 255, 255, 0.95);
2426
- border: 1px solid rgba(0, 0, 0, 0.08);
2427
- border-radius: 12px;
2428
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
2429
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2430
- }
2431
-
2432
- .backend-info-item:hover {
2433
- background: rgba(255, 255, 255, 1);
2434
- border-color: rgba(59, 130, 246, 0.3);
2435
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
2436
- }
2437
-
2438
- .info-label {
2439
- font-size: 0.75rem;
2440
- font-weight: 700;
2441
- color: #64748b;
2442
- text-transform: uppercase;
2443
- letter-spacing: 0.08em;
2444
- }
2445
-
2446
- .info-value {
2447
- font-size: 1.125rem;
2448
- font-weight: 700;
2449
- color: #0f172a;
2450
- font-family: 'Manrope', sans-serif;
2451
- }
2452
-
2453
- .fear-greed-card {
2454
- position: relative;
2455
- overflow: hidden;
2456
- }
2457
-
2458
- .fear-greed-card::before {
2459
- content: '';
2460
- position: absolute;
2461
- top: 0;
2462
- left: 0;
2463
- right: 0;
2464
- height: 3px;
2465
- background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
2466
- opacity: 0.6;
2467
- }
2468
-
2469
- .fear-greed-value {
2470
- font-size: 2.5rem !important;
2471
- font-weight: 800 !important;
2472
- line-height: 1;
2473
- }
2474
-
2475
- .fear-greed-classification {
2476
- font-size: 0.875rem;
2477
- font-weight: 600;
2478
- margin-top: 8px;
2479
- text-transform: uppercase;
2480
- letter-spacing: 0.05em;
2481
- }
2482
-
2483
- .fear-greed-gauge {
2484
- margin-top: 16px;
2485
- }
2486
-
2487
- .gauge-bar {
2488
- background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
2489
- height: 8px;
2490
- border-radius: 4px;
2491
- position: relative;
2492
- overflow: visible;
2493
- }
2494
-
2495
- .gauge-indicator {
2496
- position: absolute;
2497
- top: 50%;
2498
- transform: translate(-50%, -50%);
2499
- width: 16px;
2500
- height: 16px;
2501
- border: 2px solid #fff;
2502
- border-radius: 50%;
2503
- box-shadow: 0 0 8px currentColor;
2504
- transition: left 0.3s ease;
2505
- }
2506
-
2507
- .gauge-labels {
2508
- display: flex;
2509
- justify-content: space-between;
2510
- margin-top: 8px;
2511
- font-size: 0.75rem;
2512
- color: var(--text-muted);
2513
- }
2514
-
2515
- .toggle {
2516
- position: relative;
2517
- width: 44px;
2518
- height: 24px;
2519
- border-radius: 999px;
2520
- background: rgba(255, 255, 255, 0.2);
2521
- cursor: pointer;
2522
- }
2523
-
2524
- .toggle input {
2525
- position: absolute;
2526
- opacity: 0;
2527
- }
2528
-
2529
- .toggle span {
2530
- position: absolute;
2531
- top: 3px;
2532
- left: 4px;
2533
- width: 18px;
2534
- height: 18px;
2535
- border-radius: 50%;
2536
- background: #fff;
2537
- transition: transform 0.3s ease;
2538
- }
2539
-
2540
- .toggle input:checked + span {
2541
- transform: translateX(18px);
2542
- background: var(--secondary);
2543
- }
2544
-
2545
- .flash {
2546
- animation: flash 0.6s ease;
2547
- }
2548
-
2549
- @keyframes flash {
2550
- 0% { background: rgba(34, 197, 94, 0.2); }
2551
- 100% { background: transparent; }
2552
- }
2553
-
2554
- .table-container {
2555
- overflow-x: auto;
2556
- border-radius: 16px;
2557
- background: rgba(255, 255, 255, 0.02);
2558
- border: 1px solid var(--glass-border);
2559
- }
2560
-
2561
- .chip {
2562
- display: inline-flex;
2563
- align-items: center;
2564
- padding: 6px 12px;
2565
- border-radius: 999px;
2566
- background: var(--glass-bg-light);
2567
- border: 1px solid var(--glass-border);
2568
- color: var(--text-secondary);
2569
- font-size: 0.8125rem;
2570
- font-weight: 500;
2571
- font-family: 'Inter', sans-serif;
2572
- transition: all 0.2s ease;
2573
- }
2574
-
2575
- .chip:hover {
2576
- background: var(--glass-bg);
2577
- border-color: var(--glass-border-strong);
2578
- color: var(--text-primary);
2579
- }
2580
-
2581
- /* Modern Sentiment UI - Professional Design */
2582
- .sentiment-modern {
2583
- display: flex;
2584
- flex-direction: column;
2585
- gap: 1.75rem;
2586
- }
2587
-
2588
- .sentiment-header {
2589
- display: flex;
2590
- justify-content: space-between;
2591
- align-items: center;
2592
- margin-bottom: 0.75rem;
2593
- padding-bottom: 1rem;
2594
- border-bottom: 2px solid rgba(255, 255, 255, 0.1);
2595
- }
2596
-
2597
- .sentiment-header h4 {
2598
- margin: 0;
2599
- font-size: 1.25rem;
2600
- font-weight: 700;
2601
- font-family: 'Manrope', 'DM Sans', sans-serif;
2602
- letter-spacing: -0.02em;
2603
- background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
2604
- -webkit-background-clip: text;
2605
- -webkit-text-fill-color: transparent;
2606
- background-clip: text;
2607
- }
2608
-
2609
- .sentiment-badge {
2610
- display: inline-flex;
2611
- align-items: center;
2612
- gap: 0.5rem;
2613
- padding: 6px 14px;
2614
- border-radius: 999px;
2615
- background: linear-gradient(135deg, var(--primary-glow), var(--secondary-glow));
2616
- border: 1px solid var(--primary);
2617
- font-size: 0.75rem;
2618
- font-weight: 700;
2619
- text-transform: uppercase;
2620
- letter-spacing: 0.1em;
2621
- color: var(--primary-light);
2622
- box-shadow: 0 4px 12px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2);
2623
- font-family: 'Manrope', sans-serif;
2624
- }
2625
-
2626
- .sentiment-cards {
2627
- display: flex;
2628
- flex-direction: column;
2629
- gap: 1.25rem;
2630
- }
2631
-
2632
- .sentiment-item {
2633
- display: flex;
2634
- flex-direction: column;
2635
- gap: 0.75rem;
2636
- padding: 1.25rem;
2637
- background: var(--glass-bg);
2638
- border-radius: 16px;
2639
- border: 1px solid var(--glass-border);
2640
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2641
- position: relative;
2642
- overflow: hidden;
2643
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2644
- backdrop-filter: blur(10px);
2645
- }
2646
-
2647
- .sentiment-item::before {
2648
- content: '';
2649
- position: absolute;
2650
- top: 0;
2651
- left: 0;
2652
- width: 4px;
2653
- height: 100%;
2654
- background: currentColor;
2655
- opacity: 0.6;
2656
- transform: scaleY(0);
2657
- transform-origin: bottom;
2658
- transition: transform 0.3s ease;
2659
- }
2660
-
2661
- .sentiment-item:hover {
2662
- background: var(--glass-bg-strong);
2663
- border-color: var(--glass-border-strong);
2664
- transform: translateX(6px) translateY(-2px);
2665
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15), var(--shadow-glow-primary);
2666
- }
2667
-
2668
- .sentiment-item:hover::before {
2669
- transform: scaleY(1);
2670
- }
2671
-
2672
- .sentiment-item-header {
2673
- display: flex;
2674
- align-items: center;
2675
- gap: 0.75rem;
2676
- }
2677
-
2678
- .sentiment-icon {
2679
- display: flex;
2680
- align-items: center;
2681
- justify-content: center;
2682
- width: 40px;
2683
- height: 40px;
2684
- border-radius: 12px;
2685
- flex-shrink: 0;
2686
- transition: all 0.3s ease;
2687
- }
2688
-
2689
- .sentiment-item:hover .sentiment-icon {
2690
- transform: scale(1.15) rotate(5deg);
2691
- }
2692
-
2693
- .sentiment-item.bullish {
2694
- color: #22c55e;
2695
- }
2696
-
2697
- .sentiment-item.bullish .sentiment-icon {
2698
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.25), rgba(16, 185, 129, 0.2));
2699
- color: #22c55e;
2700
- border: 1px solid rgba(34, 197, 94, 0.3);
2701
- box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2702
- }
2703
-
2704
- .sentiment-item.neutral {
2705
- color: #38bdf8;
2706
- }
2707
-
2708
- .sentiment-item.neutral .sentiment-icon {
2709
- background: linear-gradient(135deg, rgba(56, 189, 248, 0.25), rgba(14, 165, 233, 0.2));
2710
- color: #38bdf8;
2711
- border: 1px solid rgba(56, 189, 248, 0.3);
2712
- box-shadow: 0 4px 12px rgba(56, 189, 248, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2713
- }
2714
-
2715
- .sentiment-item.bearish {
2716
- color: #ef4444;
2717
- }
2718
-
2719
- .sentiment-item.bearish .sentiment-icon {
2720
- background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.2));
2721
- color: #ef4444;
2722
- border: 1px solid rgba(239, 68, 68, 0.3);
2723
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2724
- }
2725
-
2726
- .sentiment-label {
2727
- flex: 1;
2728
- font-size: 1rem;
2729
- font-weight: 600;
2730
- font-family: 'Manrope', 'DM Sans', sans-serif;
2731
- color: var(--text-primary);
2732
- letter-spacing: -0.01em;
2733
- }
2734
-
2735
- .sentiment-percent {
2736
- font-size: 1.125rem;
2737
- font-weight: 800;
2738
- font-family: 'Manrope', 'DM Sans', sans-serif;
2739
- color: var(--text-primary);
2740
- letter-spacing: -0.02em;
2741
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
2742
- }
2743
-
2744
- .sentiment-progress {
2745
- width: 100%;
2746
- height: 10px;
2747
- background: rgba(0, 0, 0, 0.3);
2748
- border-radius: 999px;
2749
- overflow: hidden;
2750
- position: relative;
2751
- border: 1px solid rgba(255, 255, 255, 0.05);
2752
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
2753
- }
2754
-
2755
- .sentiment-progress-bar {
2756
- height: 100%;
2757
- border-radius: 999px;
2758
- transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
2759
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2);
2760
- position: relative;
2761
- overflow: hidden;
2762
- }
2763
-
2764
- .sentiment-progress-bar::after {
2765
- content: '';
2766
- position: absolute;
2767
- top: 0;
2768
- left: 0;
2769
- right: 0;
2770
- bottom: 0;
2771
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
2772
- animation: shimmer 2s infinite;
2773
- }
2774
-
2775
- @keyframes shimmer {
2776
- 0% { transform: translateX(-100%); }
2777
- 100% { transform: translateX(100%); }
2778
- }
2779
-
2780
- .sentiment-summary {
2781
- display: flex;
2782
- gap: 2rem;
2783
- padding: 1.25rem;
2784
- background: rgba(255, 255, 255, 0.03);
2785
- border-radius: 14px;
2786
- border: 1px solid rgba(255, 255, 255, 0.08);
2787
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
2788
- }
2789
-
2790
- .sentiment-summary-item {
2791
- display: flex;
2792
- flex-direction: column;
2793
- gap: 0.5rem;
2794
- flex: 1;
2795
- }
2796
-
2797
- .summary-label {
2798
- font-size: 0.75rem;
2799
- text-transform: uppercase;
2800
- letter-spacing: 0.1em;
2801
- color: var(--text-muted);
2802
- font-weight: 600;
2803
- font-family: 'Manrope', sans-serif;
2804
- }
2805
-
2806
- .summary-value {
2807
- font-size: 1.5rem;
2808
- font-weight: 800;
2809
- font-family: 'Manrope', 'DM Sans', sans-serif;
2810
- letter-spacing: -0.02em;
2811
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
2812
- }
2813
-
2814
- .summary-value.bullish {
2815
- color: #22c55e;
2816
- text-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
2817
- }
2818
-
2819
- .summary-value.neutral {
2820
- color: #38bdf8;
2821
- text-shadow: 0 0 20px rgba(56, 189, 248, 0.4);
2822
- }
2823
-
2824
- .summary-value.bearish {
2825
- color: #ef4444;
2826
- text-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
2827
- }
2828
-
2829
- @keyframes fadeIn {
2830
- from { opacity: 0; transform: translateY(8px); }
2831
- to { opacity: 1; transform: translateY(0); }
2832
- }
2833
-
2834
- /* Chart Lab Styles */
2835
- .chart-controls {
2836
- display: flex;
2837
- flex-direction: column;
2838
- gap: 1.5rem;
2839
- padding: 1.5rem;
2840
- background: var(--glass-bg);
2841
- border: 1px solid var(--glass-border);
2842
- border-radius: 20px;
2843
- backdrop-filter: blur(20px);
2844
- }
2845
-
2846
- .chart-label {
2847
- display: block;
2848
- font-size: 0.8125rem;
2849
- font-weight: 600;
2850
- text-transform: uppercase;
2851
- letter-spacing: 0.05em;
2852
- color: var(--text-muted);
2853
- margin-bottom: 0.5rem;
2854
- font-family: 'Manrope', sans-serif;
2855
- }
2856
-
2857
- .chart-symbol-selector {
2858
- flex: 1;
2859
- }
2860
-
2861
- .combobox-wrapper {
2862
- position: relative;
2863
- }
2864
-
2865
- .combobox-input {
2866
- width: 100%;
2867
- padding: 12px 16px;
2868
- background: rgba(255, 255, 255, 0.05);
2869
- border: 1px solid var(--glass-border);
2870
- border-radius: 12px;
2871
- color: var(--text-primary);
2872
- font-family: 'Manrope', 'DM Sans', sans-serif;
2873
- font-size: 0.9375rem;
2874
- transition: all 0.2s ease;
2875
- }
2876
-
2877
- .combobox-input:focus {
2878
- outline: none;
2879
- border-color: var(--primary);
2880
- background: rgba(255, 255, 255, 0.08);
2881
- box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
2882
- }
2883
-
2884
- .combobox-dropdown {
2885
- position: absolute;
2886
- top: 100%;
2887
- left: 0;
2888
- right: 0;
2889
- margin-top: 0.5rem;
2890
- background: var(--glass-bg);
2891
- border: 1px solid var(--glass-border);
2892
- border-radius: 12px;
2893
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
2894
- backdrop-filter: blur(20px);
2895
- z-index: 100;
2896
- max-height: 300px;
2897
- overflow-y: auto;
2898
- }
2899
-
2900
- .combobox-options {
2901
- padding: 0.5rem;
2902
- }
2903
-
2904
- .combobox-option {
2905
- display: flex;
2906
- justify-content: space-between;
2907
- align-items: center;
2908
- padding: 10px 14px;
2909
- border-radius: 8px;
2910
- cursor: pointer;
2911
- transition: all 0.2s ease;
2912
- font-family: 'Manrope', sans-serif;
2913
- }
2914
-
2915
- .combobox-option:hover {
2916
- background: rgba(255, 255, 255, 0.1);
2917
- transform: translateX(4px);
2918
- }
2919
-
2920
- .combobox-option.disabled {
2921
- opacity: 0.5;
2922
- cursor: not-allowed;
2923
- }
2924
-
2925
- .combobox-option strong {
2926
- font-weight: 700;
2927
- color: var(--text-primary);
2928
- font-size: 0.9375rem;
2929
  }
2930
 
2931
- .combobox-option span {
 
 
2932
  color: var(--text-muted);
2933
- font-size: 0.875rem;
2934
- }
2935
-
2936
- .chart-timeframe-selector {
2937
- display: flex;
2938
- flex-direction: column;
2939
- gap: 0.5rem;
2940
- }
2941
-
2942
- .chart-actions {
2943
- display: flex;
2944
- align-items: flex-end;
2945
  }
2946
 
2947
- .chart-container {
2948
- padding: 1.5rem;
2949
- background: rgba(0, 0, 0, 0.15);
2950
- border-radius: 16px;
2951
- border: 1px solid rgba(255, 255, 255, 0.05);
2952
  }
2953
 
2954
- .chart-header {
2955
- display: flex;
2956
- justify-content: space-between;
2957
- align-items: center;
2958
- margin-bottom: 1.5rem;
2959
- padding-bottom: 1rem;
2960
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2961
  }
2962
 
2963
- .chart-header h4 {
2964
- margin: 0;
 
 
 
 
2965
  }
2966
 
2967
- .chart-legend {
2968
- display: flex;
2969
- gap: 2rem;
2970
- flex-wrap: wrap;
2971
- }
 
2972
 
2973
- .legend-item {
2974
- display: flex;
2975
- flex-direction: column;
2976
- gap: 0.25rem;
 
 
2977
  }
2978
 
2979
- .legend-label {
2980
- font-size: 0.7rem;
2981
- text-transform: uppercase;
 
2982
  letter-spacing: 0.08em;
2983
- color: rgba(226, 232, 240, 0.5);
2984
  font-weight: 600;
2985
- font-family: 'Manrope', sans-serif;
2986
  }
2987
 
2988
- .legend-value {
2989
- font-size: 1rem;
2990
- font-weight: 600;
2991
- font-family: 'Manrope', 'DM Sans', sans-serif;
2992
- color: var(--text-primary);
2993
- display: flex;
2994
- align-items: center;
2995
- gap: 0.25rem;
2996
- }
2997
 
2998
- .legend-arrow {
2999
- font-size: 0.875rem;
3000
- opacity: 0.8;
3001
  }
3002
 
3003
- .legend-value.positive {
3004
- color: #26a69a;
 
 
 
3005
  }
3006
 
3007
- .legend-value.negative {
3008
- color: #ef5350;
 
3009
  }
3010
 
3011
- .chart-wrapper {
3012
- position: relative;
3013
- height: 450px;
3014
- padding: 0;
3015
- background: rgba(0, 0, 0, 0.2);
3016
  border-radius: 12px;
3017
- overflow: hidden;
 
3018
  }
3019
 
3020
- .chart-wrapper canvas {
3021
- padding: 12px;
 
 
 
3022
  }
3023
 
3024
- .chart-loading {
3025
- position: absolute;
3026
- inset: 0;
3027
- display: flex;
3028
- flex-direction: column;
 
 
3029
  align-items: center;
3030
- justify-content: center;
3031
- gap: 1rem;
3032
- background: rgba(0, 0, 0, 0.3);
3033
- border-radius: 12px;
3034
- z-index: 10;
3035
  }
3036
 
3037
- .loading-spinner {
3038
- width: 40px;
3039
- height: 40px;
3040
- border: 3px solid rgba(255, 255, 255, 0.1);
3041
- border-top-color: var(--primary);
3042
- border-radius: 50%;
3043
- animation: spin 1s linear infinite;
 
3044
  }
3045
 
3046
- @keyframes spin {
3047
- to { transform: rotate(360deg); }
3048
  }
3049
 
3050
- .indicator-selector {
3051
- margin-top: 1rem;
 
 
 
 
 
 
 
3052
  }
3053
 
3054
- .indicator-selector .button-group {
3055
- display: grid;
3056
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
3057
- gap: 0.75rem;
3058
  }
3059
 
3060
- .indicator-selector button {
3061
- display: flex;
3062
- flex-direction: column;
3063
- align-items: center;
3064
- gap: 0.25rem;
3065
- padding: 12px 16px;
 
3066
  }
3067
 
3068
- .indicator-selector button span {
3069
- font-weight: 600;
3070
- font-size: 0.9375rem;
 
 
3071
  }
3072
 
3073
- .indicator-selector button small {
3074
- font-size: 0.75rem;
3075
- opacity: 0.7;
3076
- font-weight: 400;
 
 
3077
  }
3078
 
3079
- .analysis-output {
3080
- margin-top: 1.5rem;
3081
- padding-top: 1.5rem;
3082
- border-top: 1px solid rgba(255, 255, 255, 0.1);
 
 
 
3083
  }
3084
 
3085
- .analysis-loading {
3086
- display: flex;
3087
- flex-direction: column;
3088
- align-items: center;
3089
- justify-content: center;
3090
- gap: 1rem;
3091
- padding: 2rem;
 
 
 
 
 
 
3092
  }
3093
 
3094
- .analysis-results {
3095
- display: flex;
3096
- flex-direction: column;
3097
- gap: 1.5rem;
3098
  }
3099
 
3100
- .analysis-header {
3101
- display: flex;
3102
- justify-content: space-between;
 
 
3103
  align-items: center;
3104
- padding-bottom: 1rem;
3105
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
3106
- }
3107
-
3108
- .analysis-header h5 {
3109
- margin: 0;
3110
- font-size: 1.125rem;
3111
- font-weight: 700;
3112
- font-family: 'Manrope', 'DM Sans', sans-serif;
3113
- }
3114
-
3115
- .analysis-badge {
3116
- padding: 4px 12px;
3117
- border-radius: 999px;
3118
- font-size: 0.75rem;
3119
- font-weight: 700;
3120
- text-transform: uppercase;
3121
- letter-spacing: 0.05em;
3122
  }
3123
 
3124
- .analysis-badge.bullish {
3125
- background: rgba(34, 197, 94, 0.2);
3126
- color: var(--success);
3127
- border: 1px solid rgba(34, 197, 94, 0.3);
3128
  }
3129
 
3130
- .analysis-badge.bearish {
3131
- background: rgba(239, 68, 68, 0.2);
3132
- color: var(--danger);
3133
- border: 1px solid rgba(239, 68, 68, 0.3);
 
 
 
3134
  }
3135
 
3136
- .analysis-badge.neutral {
3137
- background: rgba(56, 189, 248, 0.2);
3138
- color: var(--info);
3139
- border: 1px solid rgba(56, 189, 248, 0.3);
3140
  }
3141
 
3142
- .analysis-metrics {
3143
- display: grid;
3144
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
3145
- gap: 1rem;
3146
- }
3147
 
3148
- .metric-item {
3149
- display: flex;
3150
- flex-direction: column;
3151
- gap: 0.5rem;
3152
- padding: 1rem;
3153
- background: rgba(255, 255, 255, 0.03);
3154
- border-radius: 12px;
3155
- border: 1px solid rgba(255, 255, 255, 0.05);
3156
  }
3157
 
3158
- .metric-label {
 
 
 
3159
  font-size: 0.75rem;
3160
- text-transform: uppercase;
3161
- letter-spacing: 0.05em;
3162
- color: var(--text-muted);
3163
- font-weight: 600;
3164
- }
3165
-
3166
- .metric-value {
3167
- font-size: 1.25rem;
3168
- font-weight: 700;
3169
- font-family: 'Manrope', 'DM Sans', sans-serif;
3170
- color: var(--text-primary);
3171
- }
3172
-
3173
- .metric-value.positive {
3174
- color: var(--success);
3175
  }
3176
 
3177
- .metric-value.negative {
3178
- color: var(--danger);
 
 
 
 
 
3179
  }
3180
 
3181
- .analysis-summary,
3182
- .analysis-signals {
3183
- padding: 1rem;
3184
- background: rgba(255, 255, 255, 0.03);
3185
- border-radius: 12px;
3186
- border: 1px solid rgba(255, 255, 255, 0.05);
3187
  }
3188
 
3189
- .analysis-summary h6,
3190
- .analysis-signals h6 {
3191
- margin: 0 0 0.75rem 0;
3192
- font-size: 0.9375rem;
3193
- font-weight: 600;
3194
- text-transform: uppercase;
3195
- letter-spacing: 0.05em;
3196
- color: var(--text-muted);
 
3197
  }
3198
 
3199
- .analysis-summary p {
3200
- margin: 0;
3201
- line-height: 1.6;
3202
- color: var(--text-secondary);
3203
  }
3204
 
3205
- .analysis-signals ul {
3206
- margin: 0;
3207
- padding-left: 1.5rem;
3208
- list-style: disc;
3209
  }
3210
 
3211
- .analysis-signals li {
3212
- margin-bottom: 0.5rem;
3213
- color: var(--text-secondary);
3214
- line-height: 1.6;
3215
  }
3216
 
3217
- .analysis-signals li strong {
3218
- color: var(--text-primary);
3219
- font-weight: 600;
3220
  }
3221
 
3222
  @keyframes shimmer {
 
1
  @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
2
 
3
  :root {
4
+ --bg-gradient: radial-gradient(circle at top, #172032, #05060a 60%);
5
+ --glass-bg: rgba(17, 25, 40, 0.65);
6
+ --glass-border: rgba(255, 255, 255, 0.08);
7
+ --glass-highlight: rgba(255, 255, 255, 0.15);
8
+ --primary: #8f88ff;
9
+ --primary-strong: #6c63ff;
10
+ --secondary: #16d9fa;
11
+ --accent: #f472b6;
12
+ --success: #22c55e;
13
+ --warning: #facc15;
14
+ --danger: #ef4444;
15
+ --info: #38bdf8;
16
+ --text-primary: #f8fafc;
17
+ --text-muted: rgba(248, 250, 252, 0.7);
18
+ --shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.45);
19
+ --shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.35);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  --sidebar-width: 260px;
21
  }
22
 
 
28
  margin: 0;
29
  padding: 0;
30
  min-height: 100vh;
31
+ font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
 
 
 
 
32
  background: var(--bg-gradient);
33
  color: var(--text-primary);
 
 
 
34
  }
35
 
36
  body[data-theme='light'] {
37
  --bg-gradient: radial-gradient(circle at top, #f3f6ff, #dfe5ff);
38
+ --glass-bg: rgba(255, 255, 255, 0.75);
39
+ --glass-border: rgba(15, 23, 42, 0.1);
40
+ --glass-highlight: rgba(15, 23, 42, 0.05);
 
 
 
 
 
 
 
 
41
  --text-primary: #0f172a;
42
+ --text-muted: rgba(15, 23, 42, 0.6);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
44
 
45
  .app-shell {
 
49
 
50
  .sidebar {
51
  width: var(--sidebar-width);
52
+ padding: 32px 24px;
53
+ background: linear-gradient(180deg, rgba(9, 9, 13, 0.8), rgba(9, 9, 13, 0.4));
54
+ backdrop-filter: blur(30px);
55
+ border-right: 1px solid var(--glass-border);
 
 
 
56
  display: flex;
57
  flex-direction: column;
58
  gap: 24px;
59
  position: sticky;
60
  top: 0;
61
  height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
 
64
  .brand {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  display: flex;
66
  flex-direction: column;
67
  gap: 6px;
 
 
68
  }
69
 
70
  .brand strong {
71
+ font-size: 1.3rem;
72
+ letter-spacing: 0.1em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
  .env-pill {
76
  display: inline-flex;
77
  align-items: center;
78
+ gap: 6px;
79
+ background: rgba(255, 255, 255, 0.08);
80
+ padding: 4px 10px;
81
+ border-radius: 999px;
82
+ font-size: 0.75rem;
 
 
83
  text-transform: uppercase;
84
+ letter-spacing: 0.05em;
 
 
 
 
 
 
85
  }
86
 
87
  .nav {
 
93
  .nav-button {
94
  border: none;
95
  border-radius: 14px;
96
+ padding: 12px 16px;
97
  display: flex;
98
  align-items: center;
99
+ gap: 12px;
100
  background: transparent;
101
+ color: inherit;
102
+ font-weight: 500;
 
 
103
  cursor: pointer;
104
+ transition: transform 0.3s ease, background 0.3s ease;
 
 
105
  }
106
 
107
+ .nav-button svg {
108
+ width: 22px;
109
+ height: 22px;
110
+ fill: currentColor;
111
  }
112
 
113
+ .nav-button.active,
114
+ .nav-button:hover {
115
+ background: rgba(255, 255, 255, 0.08);
116
+ transform: translateX(6px);
 
 
 
 
 
 
117
  }
118
 
119
+ .sidebar-footer {
120
+ margin-top: auto;
121
+ font-size: 0.85rem;
122
+ color: var(--text-muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
 
125
+ .main-area {
126
+ flex: 1;
127
+ padding: 32px;
128
+ display: flex;
129
+ flex-direction: column;
130
+ gap: 24px;
 
 
 
131
  }
132
 
133
+ .topbar {
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ padding: 18px 24px;
138
+ border-radius: 24px;
139
+ background: var(--glass-bg);
140
+ border: 1px solid var(--glass-border);
141
+ box-shadow: var(--shadow-soft);
142
+ backdrop-filter: blur(20px);
143
+ flex-wrap: wrap;
144
+ gap: 16px;
145
  }
146
 
147
+ .topbar h1 {
148
+ margin: 0;
149
+ font-size: 1.8rem;
150
  }
151
 
152
+ .status-group {
153
+ display: flex;
154
+ gap: 12px;
155
+ flex-wrap: wrap;
156
  }
157
 
158
+ .status-pill {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 8px;
162
+ padding: 8px 14px;
163
+ border-radius: 999px;
164
+ background: rgba(255, 255, 255, 0.05);
165
+ border: 1px solid var(--glass-border);
166
+ font-size: 0.85rem;
167
+ text-transform: uppercase;
168
+ letter-spacing: 0.05em;
169
  }
170
 
171
+ .status-dot {
172
+ width: 10px;
173
+ height: 10px;
174
+ border-radius: 50%;
175
+ background: var(--warning);
176
  }
177
 
178
+ .status-pill[data-state='ok'] .status-dot {
179
+ background: var(--success);
 
180
  }
181
 
182
+ .status-pill[data-state='warn'] .status-dot {
183
+ background: var(--warning);
 
184
  }
185
 
186
+ .status-pill[data-state='error'] .status-dot {
187
+ background: var(--danger);
 
188
  }
189
 
190
+ .page-container {
191
+ flex: 1;
 
192
  }
193
 
194
+ .page {
195
+ display: none;
196
+ animation: fadeIn 0.6s ease;
197
  }
198
 
199
+ .page.active {
200
+ display: block;
 
201
  }
202
 
203
+ .section-header {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: center;
207
+ margin-bottom: 16px;
 
 
 
 
208
  }
209
 
210
+ .section-title {
211
+ font-size: 1.3rem;
212
+ letter-spacing: 0.05em;
 
 
 
 
 
 
 
213
  }
214
 
215
+ .glass-card {
216
+ background: var(--glass-bg);
217
+ border: 1px solid var(--glass-border);
218
+ border-radius: 24px;
219
+ padding: 20px;
220
+ box-shadow: var(--shadow-strong);
221
+ position: relative;
222
+ overflow: hidden;
223
  }
224
 
225
+ .glass-card::before {
226
+ content: '';
227
+ position: absolute;
228
+ inset: 0;
229
+ background: linear-gradient(120deg, transparent, var(--glass-highlight), transparent);
230
+ opacity: 0;
231
+ transition: opacity 0.4s ease;
232
  }
233
 
234
+ .glass-card:hover::before {
235
  opacity: 1;
 
 
 
 
 
 
 
236
  }
237
 
238
+ .stats-grid {
239
+ display: grid;
240
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
241
+ gap: 18px;
242
+ margin-bottom: 24px;
 
 
 
 
 
 
 
 
 
 
243
  }
244
 
245
+ .stat-card h3 {
246
+ font-size: 0.9rem;
247
+ text-transform: uppercase;
248
+ letter-spacing: 0.08em;
249
+ color: var(--text-muted);
250
  }
251
 
252
+ .stat-value {
253
+ font-size: 1.9rem;
254
+ font-weight: 600;
255
+ margin: 12px 0 6px;
256
  }
257
 
258
+ .stat-trend {
 
 
259
  display: flex;
260
  align-items: center;
261
+ gap: 6px;
262
+ font-size: 0.85rem;
263
  }
264
 
265
+ .grid-two {
266
+ display: grid;
267
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
268
+ gap: 20px;
 
 
 
 
 
 
 
 
 
 
 
269
  }
270
 
271
+ .table-wrapper {
272
+ overflow: auto;
 
 
 
273
  }
274
 
275
+ table {
276
+ width: 100%;
277
+ border-collapse: collapse;
 
 
278
  }
279
 
280
+ th, td {
281
+ text-align: left;
282
+ padding: 12px 10px;
283
+ font-size: 0.92rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  }
285
 
286
+ th {
287
+ font-size: 0.8rem;
288
+ letter-spacing: 0.05em;
289
  color: var(--text-muted);
290
+ text-transform: uppercase;
 
 
 
 
 
 
 
 
 
 
 
291
  }
292
 
293
+ tr {
294
+ transition: background 0.3s ease, transform 0.3s ease;
 
 
 
295
  }
296
 
297
+ tbody tr:hover {
298
+ background: rgba(255, 255, 255, 0.04);
299
+ transform: translateY(-1px);
 
 
 
 
300
  }
301
 
302
+ .badge {
303
+ padding: 4px 10px;
304
+ border-radius: 999px;
305
+ font-size: 0.75rem;
306
+ letter-spacing: 0.05em;
307
+ text-transform: uppercase;
308
  }
309
 
310
+ .badge-success { background: rgba(34, 197, 94, 0.15); color: var(--success); }
311
+ .badge-danger { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
312
+ .badge-neutral { background: rgba(148, 163, 184, 0.15); color: var(--text-muted); }
313
+ .text-muted { color: var(--text-muted); }
314
+ .text-success { color: var(--success); }
315
+ .text-danger { color: var(--danger); }
316
 
317
+ .ai-result {
318
+ margin-top: 20px;
319
+ padding: 20px;
320
+ border-radius: 20px;
321
+ border: 1px solid var(--glass-border);
322
+ background: rgba(0, 0, 0, 0.2);
323
  }
324
 
325
+ .action-badge {
326
+ display: inline-flex;
327
+ padding: 6px 14px;
328
+ border-radius: 999px;
329
  letter-spacing: 0.08em;
 
330
  font-weight: 600;
331
+ margin-bottom: 10px;
332
  }
333
 
334
+ .action-buy { background: rgba(34, 197, 94, 0.18); color: var(--success); }
335
+ .action-sell { background: rgba(239, 68, 68, 0.18); color: var(--danger); }
336
+ .action-hold { background: rgba(56, 189, 248, 0.18); color: var(--info); }
 
 
 
 
 
 
337
 
338
+ .ai-insights ul {
339
+ padding-left: 20px;
 
340
  }
341
 
342
+ .chip-row {
343
+ display: flex;
344
+ gap: 8px;
345
+ flex-wrap: wrap;
346
+ margin: 12px 0;
347
  }
348
 
349
+ .news-item {
350
+ padding: 12px 0;
351
+ border-bottom: 1px solid var(--glass-border);
352
  }
353
 
354
+ .ai-block {
355
+ padding: 14px;
 
 
 
356
  border-radius: 12px;
357
+ border: 1px dashed var(--glass-border);
358
+ margin-top: 12px;
359
  }
360
 
361
+ .controls-bar {
362
+ display: flex;
363
+ flex-wrap: wrap;
364
+ gap: 12px;
365
+ margin-bottom: 16px;
366
  }
367
 
368
+ .input-chip {
369
+ border: 1px solid var(--glass-border);
370
+ background: rgba(255, 255, 255, 0.03);
371
+ border-radius: 999px;
372
+ padding: 8px 14px;
373
+ color: var(--text-muted);
374
+ display: inline-flex;
375
  align-items: center;
376
+ gap: 10px;
 
 
 
 
377
  }
378
 
379
+ input[type='text'], select, textarea {
380
+ width: 100%;
381
+ background: rgba(255, 255, 255, 0.02);
382
+ border: 1px solid var(--glass-border);
383
+ border-radius: 14px;
384
+ padding: 12px 14px;
385
+ color: var(--text-primary);
386
+ font-family: inherit;
387
  }
388
 
389
+ textarea {
390
+ min-height: 100px;
391
  }
392
 
393
+ button.primary {
394
+ background: linear-gradient(120deg, var(--primary), var(--secondary));
395
+ border: none;
396
+ border-radius: 999px;
397
+ color: #fff;
398
+ padding: 12px 24px;
399
+ font-weight: 600;
400
+ cursor: pointer;
401
+ transition: transform 0.3s ease;
402
  }
403
 
404
+ button.primary:hover {
405
+ transform: translateY(-2px) scale(1.01);
 
 
406
  }
407
 
408
+ button.ghost {
409
+ background: transparent;
410
+ border: 1px solid var(--glass-border);
411
+ border-radius: 999px;
412
+ padding: 10px 20px;
413
+ color: inherit;
414
+ cursor: pointer;
415
  }
416
 
417
+ .skeleton {
418
+ position: relative;
419
+ overflow: hidden;
420
+ background: rgba(255, 255, 255, 0.05);
421
+ border-radius: 12px;
422
  }
423
 
424
+ .skeleton-block {
425
+ display: inline-block;
426
+ width: 100%;
427
+ height: 12px;
428
+ border-radius: 999px;
429
+ background: rgba(255, 255, 255, 0.08);
430
  }
431
 
432
+ .skeleton::after {
433
+ content: '';
434
+ position: absolute;
435
+ inset: 0;
436
+ transform: translateX(-100%);
437
+ background: linear-gradient(120deg, transparent, rgba(255, 255, 255, 0.25), transparent);
438
+ animation: shimmer 1.5s infinite;
439
  }
440
 
441
+ .drawer {
442
+ position: fixed;
443
+ top: 0;
444
+ right: 0;
445
+ height: 100vh;
446
+ width: min(420px, 90vw);
447
+ background: rgba(5, 7, 12, 0.92);
448
+ border-left: 1px solid var(--glass-border);
449
+ transform: translateX(100%);
450
+ transition: transform 0.4s ease;
451
+ padding: 32px;
452
+ overflow-y: auto;
453
+ z-index: 40;
454
  }
455
 
456
+ .drawer.active {
457
+ transform: translateX(0);
 
 
458
  }
459
 
460
+ .modal-backdrop {
461
+ position: fixed;
462
+ inset: 0;
463
+ background: rgba(2, 6, 23, 0.7);
464
+ display: none;
465
  align-items: center;
466
+ justify-content: center;
467
+ z-index: 50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
 
470
+ .modal-backdrop.active {
471
+ display: flex;
 
 
472
  }
473
 
474
+ .modal {
475
+ width: min(640px, 90vw);
476
+ background: var(--glass-bg);
477
+ border-radius: 28px;
478
+ padding: 28px;
479
+ border: 1px solid var(--glass-border);
480
+ backdrop-filter: blur(20px);
481
  }
482
 
483
+ .inline-message {
484
+ border-radius: 16px;
485
+ padding: 16px 18px;
486
+ border: 1px solid var(--glass-border);
487
  }
488
 
489
+ .inline-error { border-color: rgba(239, 68, 68, 0.4); background: rgba(239, 68, 68, 0.08); }
490
+ .inline-warn { border-color: rgba(250, 204, 21, 0.4); background: rgba(250, 204, 21, 0.1); }
491
+ .inline-info { border-color: rgba(56, 189, 248, 0.4); background: rgba(56, 189, 248, 0.1); }
 
 
492
 
493
+ .log-table {
494
+ font-family: 'JetBrains Mono', 'Space Grotesk', monospace;
495
+ font-size: 0.8rem;
 
 
 
 
 
496
  }
497
 
498
+ .chip {
499
+ padding: 4px 12px;
500
+ border-radius: 999px;
501
+ background: rgba(255, 255, 255, 0.08);
502
  font-size: 0.75rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  }
504
 
505
+ .toggle {
506
+ position: relative;
507
+ width: 44px;
508
+ height: 24px;
509
+ border-radius: 999px;
510
+ background: rgba(255, 255, 255, 0.2);
511
+ cursor: pointer;
512
  }
513
 
514
+ .toggle input {
515
+ position: absolute;
516
+ opacity: 0;
 
 
 
517
  }
518
 
519
+ .toggle span {
520
+ position: absolute;
521
+ top: 3px;
522
+ left: 4px;
523
+ width: 18px;
524
+ height: 18px;
525
+ border-radius: 50%;
526
+ background: #fff;
527
+ transition: transform 0.3s ease;
528
  }
529
 
530
+ .toggle input:checked + span {
531
+ transform: translateX(18px);
532
+ background: var(--secondary);
 
533
  }
534
 
535
+ .flash {
536
+ animation: flash 0.6s ease;
 
 
537
  }
538
 
539
+ @keyframes flash {
540
+ 0% { background: rgba(34, 197, 94, 0.2); }
541
+ 100% { background: transparent; }
 
542
  }
543
 
544
+ @keyframes fadeIn {
545
+ from { opacity: 0; transform: translateY(8px); }
546
+ to { opacity: 1; transform: translateY(0); }
547
  }
548
 
549
  @keyframes shimmer {
static/css/toast.css CHANGED
@@ -1,55 +1,34 @@
1
- /**
2
- * ═══════════════════════════════════════════════════════════════════
3
- * TOAST NOTIFICATIONS — ULTRA ENTERPRISE EDITION
4
- * Crypto Monitor HF — Glass + Neon Toast System
5
- * ═══════════════════════════════════════════════════════════════════
6
- */
7
-
8
- /* ═══════════════════════════════════════════════════════════════════
9
- TOAST CONTAINER
10
- ═══════════════════════════════════════════════════════════════════ */
11
-
12
- #alerts-container {
13
  position: fixed;
14
- top: calc(var(--header-height) + var(--status-bar-height) + var(--space-6));
15
- right: var(--space-6);
16
- z-index: var(--z-toast);
17
  display: flex;
18
  flex-direction: column;
19
- gap: var(--space-3);
20
- max-width: 420px;
21
- width: 100%;
22
- pointer-events: none;
23
  }
24
 
25
- /* ═══════════════════════════════════════════════════════════════════
26
- TOAST BASE
27
- ═══════════════════════════════════════════════════════════════════ */
28
-
29
  .toast {
30
- background: var(--toast-bg);
31
- border: 1px solid var(--border-medium);
32
- border-left-width: 4px;
33
- border-radius: var(--radius-md);
34
- backdrop-filter: var(--blur-lg);
35
- box-shadow: var(--shadow-lg);
36
- padding: var(--space-4) var(--space-5);
37
  display: flex;
38
- align-items: start;
39
- gap: var(--space-3);
40
- pointer-events: all;
41
- animation: toast-slide-in 0.3s var(--ease-spring);
42
  position: relative;
43
  overflow: hidden;
44
  }
45
 
46
- .toast.removing {
47
- animation: toast-slide-out 0.25s var(--ease-in) forwards;
48
- }
49
-
50
- @keyframes toast-slide-in {
51
  from {
52
- transform: translateX(120%);
53
  opacity: 0;
54
  }
55
  to {
@@ -58,181 +37,130 @@
58
  }
59
  }
60
 
61
- @keyframes toast-slide-out {
 
 
 
 
 
 
 
 
62
  to {
63
- transform: translateX(120%);
64
  opacity: 0;
65
  }
66
  }
67
 
68
- /* ═══════════════════════════════════════════════════════════════════
69
- TOAST VARIANTS
70
- ═══════════════════════════════════════════════════════════════════ */
71
-
72
- .toast-success {
73
- border-left-color: var(--success);
74
- box-shadow: var(--shadow-lg), 0 0 0 1px rgba(34, 197, 94, 0.20);
75
- }
76
-
77
- .toast-error {
78
- border-left-color: var(--danger);
79
- box-shadow: var(--shadow-lg), 0 0 0 1px rgba(239, 68, 68, 0.20);
80
- }
81
-
82
- .toast-warning {
83
- border-left-color: var(--warning);
84
- box-shadow: var(--shadow-lg), 0 0 0 1px rgba(245, 158, 11, 0.20);
85
- }
86
-
87
- .toast-info {
88
- border-left-color: var(--info);
89
- box-shadow: var(--shadow-lg), 0 0 0 1px rgba(14, 165, 233, 0.20);
90
- }
91
-
92
- /* ═══════════════════════════════════════════════════════════════════
93
- TOAST CONTENT
94
- ═══════════════════════════════════════════════════════════════════ */
95
-
96
  .toast-icon {
97
- flex-shrink: 0;
98
- width: 20px;
99
- height: 20px;
100
  display: flex;
101
  align-items: center;
102
  justify-content: center;
103
- }
104
-
105
- .toast-success .toast-icon {
106
- color: var(--success);
107
- }
108
-
109
- .toast-error .toast-icon {
110
- color: var(--danger);
111
- }
112
-
113
- .toast-warning .toast-icon {
114
- color: var(--warning);
115
- }
116
-
117
- .toast-info .toast-icon {
118
- color: var(--info);
119
  }
120
 
121
  .toast-content {
122
  flex: 1;
123
- display: flex;
124
- flex-direction: column;
125
- gap: var(--space-1);
126
  }
127
 
128
  .toast-title {
129
- font-size: var(--fs-sm);
130
- font-weight: var(--fw-semibold);
131
- color: var(--text-strong);
132
- margin: 0;
133
  }
134
 
135
  .toast-message {
136
- font-size: var(--fs-xs);
137
- color: var(--text-soft);
138
- line-height: var(--lh-relaxed);
139
  }
140
 
141
- /* ═══════════════════════════════════════════════════════════════════
142
- TOAST CLOSE BUTTON
143
- ═══════════════════════════════════════════════════════════════════ */
144
-
145
  .toast-close {
146
- flex-shrink: 0;
147
  width: 24px;
148
  height: 24px;
 
 
 
 
 
149
  display: flex;
150
  align-items: center;
151
  justify-content: center;
152
- background: transparent;
153
- border: none;
154
- color: var(--text-muted);
155
- cursor: pointer;
156
- border-radius: var(--radius-xs);
157
- transition: all var(--transition-fast);
158
  }
159
 
160
  .toast-close:hover {
161
- background: var(--surface-glass);
162
- color: var(--text-normal);
163
  }
164
 
165
- /* ═══════════════════════════════════════════════════════════════════
166
- TOAST PROGRESS BAR
167
- ═══════════════════════════════════════════════════════════════════ */
168
-
169
  .toast-progress {
170
  position: absolute;
171
  bottom: 0;
172
  left: 0;
173
  height: 3px;
174
- background: currentColor;
175
- opacity: 0.4;
176
- animation: toast-progress-shrink 5s linear forwards;
177
  }
178
 
179
- @keyframes toast-progress-shrink {
180
- from {
181
- width: 100%;
182
- }
183
- to {
184
- width: 0%;
185
- }
 
186
  }
187
 
188
- .toast-success .toast-progress {
 
189
  color: var(--success);
190
  }
191
 
192
- .toast-error .toast-progress {
 
 
 
 
 
193
  color: var(--danger);
194
  }
195
 
196
- .toast-warning .toast-progress {
 
 
 
 
 
197
  color: var(--warning);
198
  }
199
 
200
- .toast-info .toast-progress {
201
- color: var(--info);
202
  }
203
 
204
- /* ═══════════════════════════════════════════════════════════════════
205
- MOBILE ADJUSTMENTS
206
- ═══════════════════════════════════════════════════════════════════ */
 
207
 
 
208
  @media (max-width: 768px) {
209
- #alerts-container {
210
- top: auto;
211
- bottom: calc(var(--mobile-nav-height) + var(--space-4));
212
- right: var(--space-4);
213
- left: var(--space-4);
214
  max-width: none;
215
  }
216
-
217
- @keyframes toast-slide-in {
218
- from {
219
- transform: translateY(120%);
220
- opacity: 0;
221
- }
222
- to {
223
- transform: translateY(0);
224
- opacity: 1;
225
- }
226
- }
227
-
228
- @keyframes toast-slide-out {
229
- to {
230
- transform: translateY(120%);
231
- opacity: 0;
232
- }
233
  }
234
  }
235
-
236
- /* ═══════════════════��═══════════════════════════════════════════════
237
- END OF TOAST
238
- ═══════════════════════════════════════════════════════════════════ */
 
1
+ /* Toast Notification System */
2
+ .toast-container {
 
 
 
 
 
 
 
 
 
 
3
  position: fixed;
4
+ top: 20px;
5
+ right: 20px;
6
+ z-index: 10000;
7
  display: flex;
8
  flex-direction: column;
9
+ gap: 10px;
10
+ max-width: 400px;
 
 
11
  }
12
 
 
 
 
 
13
  .toast {
14
+ background: rgba(17, 24, 39, 0.95);
15
+ backdrop-filter: blur(20px);
16
+ border: 1px solid var(--border);
17
+ border-radius: 12px;
18
+ padding: 16px 20px;
19
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
 
20
  display: flex;
21
+ align-items: center;
22
+ gap: 12px;
23
+ animation: slideIn 0.3s ease;
24
+ min-width: 300px;
25
  position: relative;
26
  overflow: hidden;
27
  }
28
 
29
+ @keyframes slideIn {
 
 
 
 
30
  from {
31
+ transform: translateX(400px);
32
  opacity: 0;
33
  }
34
  to {
 
37
  }
38
  }
39
 
40
+ .toast.removing {
41
+ animation: slideOut 0.3s ease forwards;
42
+ }
43
+
44
+ @keyframes slideOut {
45
+ from {
46
+ transform: translateX(0);
47
+ opacity: 1;
48
+ }
49
  to {
50
+ transform: translateX(400px);
51
  opacity: 0;
52
  }
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  .toast-icon {
56
+ width: 40px;
57
+ height: 40px;
58
+ border-radius: 10px;
59
  display: flex;
60
  align-items: center;
61
  justify-content: center;
62
+ font-size: 18px;
63
+ flex-shrink: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  }
65
 
66
  .toast-content {
67
  flex: 1;
 
 
 
68
  }
69
 
70
  .toast-title {
71
+ font-weight: 600;
72
+ font-size: 14px;
73
+ margin-bottom: 4px;
74
+ color: var(--text-primary);
75
  }
76
 
77
  .toast-message {
78
+ font-size: 13px;
79
+ color: var(--text-secondary);
80
+ line-height: 1.4;
81
  }
82
 
 
 
 
 
83
  .toast-close {
 
84
  width: 24px;
85
  height: 24px;
86
+ border-radius: 6px;
87
+ background: rgba(255, 255, 255, 0.1);
88
+ border: none;
89
+ color: var(--text-secondary);
90
+ cursor: pointer;
91
  display: flex;
92
  align-items: center;
93
  justify-content: center;
94
+ transition: all 0.2s;
95
+ flex-shrink: 0;
 
 
 
 
96
  }
97
 
98
  .toast-close:hover {
99
+ background: rgba(255, 255, 255, 0.2);
100
+ color: var(--text-primary);
101
  }
102
 
 
 
 
 
103
  .toast-progress {
104
  position: absolute;
105
  bottom: 0;
106
  left: 0;
107
  height: 3px;
108
+ background: var(--primary);
109
+ animation: progress 5s linear forwards;
 
110
  }
111
 
112
+ @keyframes progress {
113
+ from { width: 100%; }
114
+ to { width: 0%; }
115
+ }
116
+
117
+ /* Toast Types */
118
+ .toast.success {
119
+ border-left: 4px solid var(--success);
120
  }
121
 
122
+ .toast.success .toast-icon {
123
+ background: rgba(16, 185, 129, 0.2);
124
  color: var(--success);
125
  }
126
 
127
+ .toast.error {
128
+ border-left: 4px solid var(--danger);
129
+ }
130
+
131
+ .toast.error .toast-icon {
132
+ background: rgba(239, 68, 68, 0.2);
133
  color: var(--danger);
134
  }
135
 
136
+ .toast.warning {
137
+ border-left: 4px solid var(--warning);
138
+ }
139
+
140
+ .toast.warning .toast-icon {
141
+ background: rgba(245, 158, 11, 0.2);
142
  color: var(--warning);
143
  }
144
 
145
+ .toast.info {
146
+ border-left: 4px solid var(--info);
147
  }
148
 
149
+ .toast.info .toast-icon {
150
+ background: rgba(59, 130, 246, 0.2);
151
+ color: var(--info);
152
+ }
153
 
154
+ /* Mobile Responsive */
155
  @media (max-width: 768px) {
156
+ .toast-container {
157
+ top: 10px;
158
+ right: 10px;
159
+ left: 10px;
 
160
  max-width: none;
161
  }
162
+
163
+ .toast {
164
+ min-width: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  }
166
  }
 
 
 
 
static/js/aiAdvisorView.js CHANGED
@@ -1,94 +1,129 @@
1
  import apiClient from './apiClient.js';
 
2
 
3
  class AIAdvisorView {
4
  constructor(section) {
5
  this.section = section;
6
- this.queryForm = section?.querySelector('[data-query-form]');
7
- this.sentimentForm = section?.querySelector('[data-sentiment-form]');
8
- this.queryOutput = section?.querySelector('[data-query-output]');
9
- this.sentimentOutput = section?.querySelector('[data-sentiment-output]');
 
 
10
  }
11
 
12
  init() {
13
- if (this.queryForm) {
14
- this.queryForm.addEventListener('submit', async (event) => {
15
- event.preventDefault();
16
- const formData = new FormData(this.queryForm);
17
- await this.handleQuery(formData);
18
- });
19
- }
20
- if (this.sentimentForm) {
21
- this.sentimentForm.addEventListener('submit', async (event) => {
22
- event.preventDefault();
23
- const formData = new FormData(this.sentimentForm);
24
- await this.handleSentiment(formData);
25
- });
26
- }
27
  }
28
 
29
- async handleQuery(formData) {
30
- const query = formData.get('query') || '';
31
- if (!query.trim()) return;
32
-
33
- if (this.queryOutput) {
34
- this.queryOutput.innerHTML = '<p>Processing query...</p>';
 
 
 
35
  }
36
-
37
- const result = await apiClient.runQuery({ query });
38
- if (!result.ok) {
39
- if (this.queryOutput) {
40
- this.queryOutput.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
41
- }
42
- return;
43
  }
44
-
45
- // Backend returns {success: true, type: ..., message: ..., data: ...}
46
- const data = result.data || {};
47
- if (this.queryOutput) {
48
- this.queryOutput.innerHTML = `
49
- <div class="glass-card">
50
- <h4>AI Response</h4>
51
- <p><strong>Type:</strong> ${data.type || 'general'}</p>
52
- <p>${data.message || 'Query processed'}</p>
53
- ${data.data ? `<pre>${JSON.stringify(data.data, null, 2)}</pre>` : ''}
54
- </div>
55
- `;
56
  }
57
- }
58
 
59
- async handleSentiment(formData) {
60
- const text = formData.get('text') || '';
61
- if (!text.trim()) return;
62
-
63
- if (this.sentimentOutput) {
64
- this.sentimentOutput.innerHTML = '<p>Analyzing sentiment...</p>';
65
  }
66
-
67
- const result = await apiClient.analyzeSentiment({ text });
68
- if (!result.ok) {
69
- if (this.sentimentOutput) {
70
- this.sentimentOutput.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
 
71
  }
72
- return;
73
  }
74
-
75
- // Backend returns {success: true, sentiment: ..., confidence: ..., details: ...}
76
- const data = result.data || {};
77
- const sentiment = data.sentiment || 'neutral';
78
- const confidence = data.confidence || 0;
79
-
80
- if (this.sentimentOutput) {
81
- this.sentimentOutput.innerHTML = `
82
- <div class="glass-card">
83
- <h4>Sentiment Analysis</h4>
84
- <p><strong>Label:</strong> <span class="chip">${sentiment}</span></p>
85
- <p><strong>Confidence:</strong> ${(confidence * 100).toFixed(1)}%</p>
86
- ${data.details ? `<pre style="font-size: 0.875rem;">${JSON.stringify(data.details, null, 2)}</pre>` : ''}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </div>
88
- `;
 
 
 
 
89
  }
90
  }
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  }
93
 
94
  export default AIAdvisorView;
 
1
  import apiClient from './apiClient.js';
2
+ import { formatCurrency, formatPercent } from './uiUtils.js';
3
 
4
  class AIAdvisorView {
5
  constructor(section) {
6
  this.section = section;
7
+ this.form = section?.querySelector('[data-ai-form]');
8
+ this.decisionContainer = section?.querySelector('[data-ai-result]');
9
+ this.sentimentContainer = section?.querySelector('[data-sentiment-result]');
10
+ this.disclaimer = section?.querySelector('[data-ai-disclaimer]');
11
+ this.contextInput = section?.querySelector('textarea[name="context"]');
12
+ this.modelSelect = section?.querySelector('select[name="model"]');
13
  }
14
 
15
  init() {
16
+ if (!this.form) return;
17
+ this.form.addEventListener('submit', async (event) => {
18
+ event.preventDefault();
19
+ const formData = new FormData(this.form);
20
+ await this.handleSubmit(formData);
21
+ });
 
 
 
 
 
 
 
 
22
  }
23
 
24
+ async handleSubmit(formData) {
25
+ const symbol = formData.get('symbol') || 'BTC';
26
+ const horizon = formData.get('horizon') || 'swing';
27
+ const risk = formData.get('risk') || 'moderate';
28
+ const context = (formData.get('context') || '').trim();
29
+ const mode = formData.get('model') || 'auto';
30
+
31
+ if (this.decisionContainer) {
32
+ this.decisionContainer.innerHTML = '<p>Generating AI strategy...</p>';
33
  }
34
+ if (this.sentimentContainer && context) {
35
+ this.sentimentContainer.innerHTML = '<p>Running sentiment model...</p>';
 
 
 
 
 
36
  }
37
+
38
+ const decisionPayload = {
39
+ query: `Provide ${horizon} outlook for ${symbol} with ${risk} risk. ${context}`,
40
+ symbol,
41
+ task: 'decision',
42
+ options: { horizon, risk },
43
+ };
44
+
45
+ const jobs = [apiClient.runQuery(decisionPayload)];
46
+ if (context) {
47
+ jobs.push(apiClient.analyzeSentiment({ text: context, mode }));
 
48
  }
 
49
 
50
+ const [decisionResult, sentimentResult] = await Promise.all(jobs);
51
+
52
+ if (!decisionResult.ok) {
53
+ this.decisionContainer.innerHTML = `<div class="inline-message inline-error">${decisionResult.error}</div>`;
54
+ } else {
55
+ this.renderDecisionResult(decisionResult.data || {});
56
  }
57
+
58
+ if (context && this.sentimentContainer) {
59
+ if (!sentimentResult?.ok) {
60
+ this.sentimentContainer.innerHTML = `<div class="inline-message inline-error">${sentimentResult?.error || 'AI sentiment endpoint unavailable'}</div>`;
61
+ } else {
62
+ this.renderSentimentResult(sentimentResult.data || sentimentResult);
63
  }
 
64
  }
65
+ }
66
+
67
+ renderDecisionResult(response) {
68
+ if (!this.decisionContainer) return;
69
+ const payload = response.data || {};
70
+ const analysis = payload.analysis || payload;
71
+ const summary = analysis.summary?.summary || analysis.summary || 'No summary provided.';
72
+ const signals = analysis.signals || {};
73
+ const topCoins = (payload.top_coins || []).slice(0, 3);
74
+
75
+ this.decisionContainer.innerHTML = `
76
+ <div class="ai-result">
77
+ <p class="text-muted">${response.message || 'Decision support summary'}</p>
78
+ <p>${summary}</p>
79
+ <div class="grid-two">
80
+ <div>
81
+ <h4>Market Signals</h4>
82
+ <ul>
83
+ ${Object.entries(signals)
84
+ .map(([, value]) => `<li>${value?.label || 'neutral'} (${value?.score ?? '—'})</li>`)
85
+ .join('') || '<li>No model signals.</li>'}
86
+ </ul>
87
+ </div>
88
+ <div>
89
+ <h4>Watchlist</h4>
90
+ <ul>
91
+ ${topCoins
92
+ .map(
93
+ (coin) =>
94
+ `<li>${coin.symbol || coin.ticker}: ${formatCurrency(coin.price)} (${formatPercent(coin.change_24h)})</li>`,
95
+ )
96
+ .join('') || '<li>No coin highlights.</li>'}
97
+ </ul>
98
+ </div>
99
  </div>
100
+ </div>
101
+ `;
102
+ if (this.disclaimer) {
103
+ this.disclaimer.textContent =
104
+ response.data?.disclaimer || 'This AI output is experimental research and not financial advice.';
105
  }
106
  }
107
 
108
+ renderSentimentResult(result) {
109
+ const container = this.sentimentContainer;
110
+ if (!container) return;
111
+ const payload = result.result || result;
112
+ const signals = result.signals || payload.signals || {};
113
+ container.innerHTML = `
114
+ <div class="glass-card">
115
+ <h4>Sentiment (${result.mode || 'auto'})</h4>
116
+ <p><strong>Label:</strong> ${payload.label || payload.classification || 'neutral'}</p>
117
+ <p><strong>Score:</strong> ${payload.score ?? payload.sentiment?.score ?? '—'}</p>
118
+ <div class="chip-row">
119
+ ${Object.entries(signals)
120
+ .map(([key, value]) => `<span class="chip">${key}: ${value?.label || 'n/a'}</span>`)
121
+ .join('') || ''}
122
+ </div>
123
+ <p>${payload.summary?.summary || payload.summary?.summary_text || payload.summary || ''}</p>
124
+ </div>
125
+ `;
126
+ }
127
  }
128
 
129
  export default AIAdvisorView;
static/js/apiClient.js CHANGED
@@ -2,16 +2,8 @@ const DEFAULT_TTL = 60 * 1000; // 1 minute cache
2
 
3
  class ApiClient {
4
  constructor() {
5
- // Use current origin by default to avoid hardcoded URLs
6
- this.baseURL = window.location.origin;
7
-
8
- // Allow override via window.BACKEND_URL if needed
9
- if (typeof window.BACKEND_URL === 'string' && window.BACKEND_URL.trim()) {
10
- this.baseURL = window.BACKEND_URL.trim().replace(/\/$/, '');
11
- }
12
-
13
- console.log('[ApiClient] Using Backend:', this.baseURL);
14
-
15
  this.cache = new Map();
16
  this.requestLogs = [];
17
  this.errorLogs = [];
@@ -136,66 +128,24 @@ class ApiClient {
136
  }
137
 
138
  // ===== Specific API helpers =====
139
- // Note: Backend uses api_server_extended.py which has different endpoints
140
-
141
  getHealth() {
142
- // Backend doesn't have /api/health, use /api/status instead
143
- return this.get('/api/status');
144
  }
145
 
146
  getTopCoins(limit = 10) {
147
- // Backend uses /api/market which returns cryptocurrencies array
148
- return this.get('/api/market').then(result => {
149
- if (result.ok && result.data && result.data.cryptocurrencies) {
150
- return {
151
- ok: true,
152
- data: result.data.cryptocurrencies.slice(0, limit)
153
- };
154
- }
155
- return result;
156
- });
157
  }
158
 
159
  getCoinDetails(symbol) {
160
- // Get from market data and filter by symbol
161
- return this.get('/api/market').then(result => {
162
- if (result.ok && result.data && result.data.cryptocurrencies) {
163
- const coin = result.data.cryptocurrencies.find(
164
- c => c.symbol.toUpperCase() === symbol.toUpperCase()
165
- );
166
- return coin ? { ok: true, data: coin } : { ok: false, error: 'Coin not found' };
167
- }
168
- return result;
169
- });
170
  }
171
 
172
  getMarketStats() {
173
- // Backend returns stats in /api/market response
174
- return this.get('/api/market').then(result => {
175
- if (result.ok && result.data) {
176
- return {
177
- ok: true,
178
- data: {
179
- total_market_cap: result.data.total_market_cap,
180
- btc_dominance: result.data.btc_dominance,
181
- total_volume_24h: result.data.total_volume_24h,
182
- market_cap_change_24h: result.data.market_cap_change_24h
183
- }
184
- };
185
- }
186
- return result;
187
- });
188
  }
189
 
190
  getLatestNews(limit = 20) {
191
- // Backend doesn't have news endpoint yet, return empty for now
192
- return Promise.resolve({
193
- ok: true,
194
- data: {
195
- articles: [],
196
- message: 'News endpoint not yet implemented in backend'
197
- }
198
- });
199
  }
200
 
201
  getProviders() {
@@ -203,112 +153,41 @@ class ApiClient {
203
  }
204
 
205
  getPriceChart(symbol, timeframe = '7d') {
206
- // Backend uses /api/ohlcv
207
- const cleanSymbol = encodeURIComponent(String(symbol || 'BTC').trim().toUpperCase());
208
- // Map timeframe to interval and limit
209
- const intervalMap = { '1d': '1h', '7d': '1h', '30d': '4h', '90d': '1d', '365d': '1d' };
210
- const limitMap = { '1d': 24, '7d': 168, '30d': 180, '90d': 90, '365d': 365 };
211
- const interval = intervalMap[timeframe] || '1h';
212
- const limit = limitMap[timeframe] || 168;
213
- return this.get(`/api/ohlcv?symbol=${cleanSymbol}USDT&interval=${interval}&limit=${limit}`);
214
  }
215
 
216
  analyzeChart(symbol, timeframe = '7d', indicators = []) {
217
- // Not implemented in backend yet
218
- return Promise.resolve({
219
- ok: false,
220
- error: 'Chart analysis not yet implemented in backend'
221
- });
222
  }
223
 
224
  runQuery(payload) {
225
- // Not implemented in backend yet
226
- return Promise.resolve({
227
- ok: false,
228
- error: 'Query endpoint not yet implemented in backend'
229
- });
230
  }
231
 
232
  analyzeSentiment(payload) {
233
- // Backend has /api/sentiment but it returns market sentiment, not text analysis
234
- // For now, return the market sentiment
235
- return this.get('/api/sentiment');
236
  }
237
 
238
  summarizeNews(item) {
239
- // Not implemented in backend yet
240
- return Promise.resolve({
241
- ok: false,
242
- error: 'News summarization not yet implemented in backend'
243
- });
244
  }
245
 
246
  getDatasetsList() {
247
- // Not implemented in backend yet
248
- return Promise.resolve({
249
- ok: true,
250
- data: {
251
- datasets: [],
252
- message: 'Datasets endpoint not yet implemented in backend'
253
- }
254
- });
255
  }
256
 
257
  getDatasetSample(name) {
258
- // Not implemented in backend yet
259
- return Promise.resolve({
260
- ok: false,
261
- error: 'Dataset sample not yet implemented in backend'
262
- });
263
  }
264
 
265
  getModelsList() {
266
- // Backend has /api/hf/models
267
- return this.get('/api/hf/models');
268
  }
269
 
270
  testModel(payload) {
271
- // Not implemented in backend yet
272
- return Promise.resolve({
273
- ok: false,
274
- error: 'Model testing not yet implemented in backend'
275
- });
276
- }
277
-
278
- // ===== Additional methods for backend compatibility =====
279
-
280
- getTrending() {
281
- return this.get('/api/trending');
282
- }
283
-
284
- getStats() {
285
- return this.get('/api/stats');
286
- }
287
-
288
- getHFHealth() {
289
- return this.get('/api/hf/health');
290
- }
291
-
292
- runDiagnostics(autoFix = false) {
293
- return this.post('/api/diagnostics/run', { auto_fix: autoFix });
294
- }
295
-
296
- getLastDiagnostics() {
297
- return this.get('/api/diagnostics/last');
298
- }
299
-
300
- runAPLScan() {
301
- return this.post('/api/apl/run');
302
- }
303
-
304
- getAPLReport() {
305
- return this.get('/api/apl/report');
306
- }
307
-
308
- getAPLSummary() {
309
- return this.get('/api/apl/summary');
310
  }
311
  }
312
 
313
  const apiClient = new ApiClient();
314
- export default apiClient;
 
2
 
3
  class ApiClient {
4
  constructor() {
5
+ const origin = window?.location?.origin ?? '';
6
+ this.baseURL = origin.replace(/\/$/, '');
 
 
 
 
 
 
 
 
7
  this.cache = new Map();
8
  this.requestLogs = [];
9
  this.errorLogs = [];
 
128
  }
129
 
130
  // ===== Specific API helpers =====
 
 
131
  getHealth() {
132
+ return this.get('/api/health');
 
133
  }
134
 
135
  getTopCoins(limit = 10) {
136
+ return this.get(`/api/coins/top?limit=${limit}`);
 
 
 
 
 
 
 
 
 
137
  }
138
 
139
  getCoinDetails(symbol) {
140
+ return this.get(`/api/coins/${symbol}`);
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
  getMarketStats() {
144
+ return this.get('/api/market/stats');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  }
146
 
147
  getLatestNews(limit = 20) {
148
+ return this.get(`/api/news/latest?limit=${limit}`);
 
 
 
 
 
 
 
149
  }
150
 
151
  getProviders() {
 
153
  }
154
 
155
  getPriceChart(symbol, timeframe = '7d') {
156
+ return this.get(`/api/charts/price/${symbol}?timeframe=${timeframe}`);
 
 
 
 
 
 
 
157
  }
158
 
159
  analyzeChart(symbol, timeframe = '7d', indicators = []) {
160
+ return this.post('/api/charts/analyze', { symbol, timeframe, indicators });
 
 
 
 
161
  }
162
 
163
  runQuery(payload) {
164
+ return this.post('/api/query', payload);
 
 
 
 
165
  }
166
 
167
  analyzeSentiment(payload) {
168
+ return this.post('/api/sentiment/analyze', payload);
 
 
169
  }
170
 
171
  summarizeNews(item) {
172
+ return this.post('/api/news/summarize', item);
 
 
 
 
173
  }
174
 
175
  getDatasetsList() {
176
+ return this.get('/api/datasets/list');
 
 
 
 
 
 
 
177
  }
178
 
179
  getDatasetSample(name) {
180
+ return this.get(`/api/datasets/sample?name=${encodeURIComponent(name)}`);
 
 
 
 
181
  }
182
 
183
  getModelsList() {
184
+ return this.get('/api/models/list');
 
185
  }
186
 
187
  testModel(payload) {
188
+ return this.post('/api/models/test', payload);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  }
190
  }
191
 
192
  const apiClient = new ApiClient();
193
+ export default apiClient;
static/js/apiExplorerView.js CHANGED
@@ -54,10 +54,8 @@ class ApiExplorerView {
54
  if (this.bodyInput) {
55
  this.bodyInput.value = preset.body || '';
56
  }
57
- const descEl = this.section.querySelector('[data-api-description]');
58
- const pathEl = this.section.querySelector('[data-api-path]');
59
- if (descEl) descEl.textContent = preset.description;
60
- if (pathEl) pathEl.textContent = preset.path;
61
  }
62
 
63
  async sendRequest() {
 
54
  if (this.bodyInput) {
55
  this.bodyInput.value = preset.body || '';
56
  }
57
+ this.section.querySelector('[data-api-description]').textContent = preset.description;
58
+ this.section.querySelector('[data-api-path]').textContent = preset.path;
 
 
59
  }
60
 
61
  async sendRequest() {
static/js/app.js CHANGED
@@ -1,10 +1,106 @@
1
  // Crypto Intelligence Hub - Main JavaScript
 
2
 
3
- // Global state
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  const AppState = {
5
  currentTab: 'dashboard',
6
  data: {},
7
- charts: {}
 
8
  };
9
 
10
  // Initialize app
@@ -72,42 +168,23 @@ function initTabs() {
72
  function loadTabData(tabId) {
73
  switch(tabId) {
74
  case 'dashboard':
 
 
75
  case 'market':
76
  loadMarketData();
77
  break;
78
- case 'monitor':
79
- loadMonitorData();
80
- break;
81
- case 'advanced':
82
- loadAdvancedData();
83
- break;
84
- case 'admin':
85
- loadAdminData();
86
- break;
87
- case 'hf':
88
- loadHFHealth();
89
- loadModels();
90
- break;
91
- case 'pools':
92
- loadPools();
93
- break;
94
- case 'logs':
95
- loadLogs();
96
- break;
97
- case 'resources':
98
- loadResources();
99
- loadAPIRegistry();
100
- break;
101
- case 'reports':
102
- loadReports();
103
- break;
104
- // Legacy tab names for backward compatibility
105
  case 'models':
106
  loadModels();
107
  break;
108
  case 'sentiment':
109
- loadSentimentModels();
110
- loadSentimentHistory();
 
 
 
 
 
 
111
  break;
112
  case 'news':
113
  loadNews();
@@ -121,6 +198,8 @@ function loadTabData(tabId) {
121
  case 'api-explorer':
122
  loadAPIEndpoints();
123
  break;
 
 
124
  }
125
  }
126
 
@@ -144,6 +223,9 @@ function loadAPIEndpoints() {
144
  { value: '/api/models/list', text: 'GET /api/models/list - List Models' },
145
  { value: '/api/models/status', text: 'GET /api/models/status - Models Status' },
146
  { value: '/api/models/data/stats', text: 'GET /api/models/data/stats - Models Statistics' },
 
 
 
147
  { value: '/api/logs/recent', text: 'GET /api/logs/recent - Recent Logs' },
148
  { value: '/api/logs/errors', text: 'GET /api/logs/errors - Error Logs' },
149
  { value: '/api/diagnostics/last', text: 'GET /api/diagnostics/last - Last Diagnostics' },
@@ -184,6 +266,21 @@ async function checkAPIStatus() {
184
 
185
  // Load Dashboard
186
  async function loadDashboard() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  try {
188
  // Load resources
189
  const resourcesRes = await fetch('/api/resources');
@@ -229,7 +326,13 @@ async function loadDashboard() {
229
  }
230
  } catch (error) {
231
  console.error('Error loading dashboard:', error);
232
- showError('Error loading dashboard');
 
 
 
 
 
 
233
  }
234
  }
235
 
@@ -275,6 +378,15 @@ function createCategoriesChart(categories) {
275
 
276
  // Load Market Data
277
  async function loadMarketData() {
 
 
 
 
 
 
 
 
 
278
  try {
279
  const response = await fetch('/api/market');
280
  const data = await response.json();
@@ -395,7 +507,12 @@ async function loadMarketData() {
395
  }
396
  } catch (error) {
397
  console.error('Error loading market data:', error);
398
- showError('Error loading market data');
 
 
 
 
 
399
  }
400
  }
401
 
@@ -411,6 +528,13 @@ function formatNumber(num) {
411
 
412
  // Load Models
413
  async function loadModels() {
 
 
 
 
 
 
 
414
  try {
415
  const response = await fetch('/api/models/list');
416
  const data = await response.json();
@@ -528,7 +652,12 @@ async function loadModels() {
528
  }
529
  } catch (error) {
530
  console.error('Error loading models:', error);
531
- showError('Error loading models');
 
 
 
 
 
532
  }
533
  }
534
 
@@ -549,7 +678,7 @@ async function initializeModels() {
549
  }
550
  }
551
 
552
- // Load Sentiment Models
553
  async function loadSentimentModels() {
554
  try {
555
  const response = await fetch('/api/models/list');
@@ -557,17 +686,31 @@ async function loadSentimentModels() {
557
 
558
  const models = data.models || data || [];
559
  const select = document.getElementById('sentiment-model');
560
- select.innerHTML = '<option value="">Select Model...</option>';
561
 
 
 
 
562
  models.filter(m => {
563
- const status = m.status || 'unknown';
564
- return status === 'available' || status === 'loaded' || !m.status;
 
 
 
 
 
 
565
  }).forEach(model => {
566
  const option = document.createElement('option');
567
- const modelId = model.model_id || model.name || model.key || 'unknown';
568
- const task = model.task || model.category || '';
569
- option.value = model.key || modelId;
570
- option.textContent = `${modelId}${task ? ` (${task})` : ''}`;
 
 
 
 
 
571
  select.appendChild(option);
572
  });
573
 
@@ -575,14 +718,18 @@ async function loadSentimentModels() {
575
  if (select.options.length === 1) {
576
  const option = document.createElement('option');
577
  option.value = '';
578
- option.textContent = 'No models available';
579
  option.disabled = true;
580
  select.appendChild(option);
581
  }
 
 
582
  } catch (error) {
583
  console.error('Error loading sentiment models:', error);
584
  const select = document.getElementById('sentiment-model');
585
- select.innerHTML = '<option value="">Error loading models</option>';
 
 
586
  }
587
  }
588
 
@@ -789,7 +936,144 @@ async function analyzeNewsSentiment() {
789
  }
790
  }
791
 
792
- // Analyze Sentiment (updated)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  async function analyzeSentiment() {
794
  const text = document.getElementById('sentiment-text').value;
795
  const mode = document.getElementById('sentiment-mode').value;
@@ -806,11 +1090,22 @@ async function analyzeSentiment() {
806
  try {
807
  let response;
808
 
809
- // Use the sentiment/analyze endpoint with mode
810
- response = await fetch('/api/sentiment/analyze', {
 
 
 
 
 
 
 
 
 
 
 
811
  method: 'POST',
812
  headers: { 'Content-Type': 'application/json' },
813
- body: JSON.stringify({ text: text, mode: mode })
814
  });
815
 
816
  const data = await response.json();
@@ -935,6 +1230,12 @@ function loadSentimentHistory() {
935
 
936
  // Load News
937
  async function loadNews() {
 
 
 
 
 
 
938
  try {
939
  // Try /api/news/latest first, fallback to /api/news
940
  let response;
@@ -1099,6 +1400,12 @@ async function loadNews() {
1099
 
1100
  // Load Providers
1101
  async function loadProviders() {
 
 
 
 
 
 
1102
  try {
1103
  // Load providers and auto-discovery health summary in parallel
1104
  const [providersRes, healthRes] = await Promise.all([
@@ -1406,6 +1713,207 @@ async function runDiagnostics() {
1406
  }
1407
  }
1408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1409
  // Test API
1410
  async function testAPI() {
1411
  const endpoint = document.getElementById('api-endpoint').value;
@@ -1490,19 +1998,13 @@ async function testAPI() {
1490
 
1491
  // Utility Functions
1492
  function showError(message) {
1493
- const alert = document.createElement('div');
1494
- alert.className = 'alert alert-error';
1495
- alert.textContent = message;
1496
- document.body.appendChild(alert);
1497
- setTimeout(() => alert.remove(), 5000);
1498
  }
1499
 
1500
  function showSuccess(message) {
1501
- const alert = document.createElement('div');
1502
- alert.className = 'alert alert-success';
1503
- alert.textContent = message;
1504
- document.body.appendChild(alert);
1505
- setTimeout(() => alert.remove(), 5000);
1506
  }
1507
 
1508
  // Additional tab loaders for HTML tabs
@@ -1839,3 +2341,299 @@ loadDashboard = async function() {
1839
  await originalLoadDashboard();
1840
  updateHeaderStats();
1841
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // Crypto Intelligence Hub - Main JavaScript
2
+ // Enhanced Pro Trading Terminal UI
3
 
4
+ // =============================================================================
5
+ // Toast Notification System
6
+ // =============================================================================
7
+ const ToastManager = {
8
+ container: null,
9
+
10
+ init() {
11
+ this.container = document.getElementById('toast-container');
12
+ if (!this.container) {
13
+ this.container = document.createElement('div');
14
+ this.container.id = 'toast-container';
15
+ this.container.className = 'toast-container';
16
+ document.body.appendChild(this.container);
17
+ }
18
+ },
19
+
20
+ show(message, type = 'info', duration = 5000) {
21
+ if (!this.container) this.init();
22
+
23
+ const toast = document.createElement('div');
24
+ toast.className = `toast ${type}`;
25
+
26
+ const icons = {
27
+ success: 'fa-check-circle',
28
+ error: 'fa-times-circle',
29
+ warning: 'fa-exclamation-triangle',
30
+ info: 'fa-info-circle'
31
+ };
32
+
33
+ const titles = {
34
+ success: 'Success',
35
+ error: 'Error',
36
+ warning: 'Warning',
37
+ info: 'Info'
38
+ };
39
+
40
+ toast.innerHTML = `
41
+ <div class="toast-icon">
42
+ <i class="fas ${icons[type] || icons.info}"></i>
43
+ </div>
44
+ <div class="toast-content">
45
+ <div class="toast-title">${titles[type] || titles.info}</div>
46
+ <div class="toast-message">${message}</div>
47
+ </div>
48
+ <button class="toast-close" onclick="ToastManager.remove(this.parentElement)">
49
+ <i class="fas fa-times"></i>
50
+ </button>
51
+ <div class="toast-progress"></div>
52
+ `;
53
+
54
+ this.container.appendChild(toast);
55
+
56
+ // Auto remove after duration
57
+ if (duration > 0) {
58
+ setTimeout(() => this.remove(toast), duration);
59
+ }
60
+
61
+ return toast;
62
+ },
63
+
64
+ remove(toast) {
65
+ if (!toast) return;
66
+ toast.classList.add('removing');
67
+ setTimeout(() => {
68
+ if (toast.parentElement) {
69
+ toast.parentElement.removeChild(toast);
70
+ }
71
+ }, 300);
72
+ },
73
+
74
+ success(message, duration) {
75
+ return this.show(message, 'success', duration);
76
+ },
77
+
78
+ error(message, duration) {
79
+ return this.show(message, 'error', duration);
80
+ },
81
+
82
+ warning(message, duration) {
83
+ return this.show(message, 'warning', duration);
84
+ },
85
+
86
+ info(message, duration) {
87
+ return this.show(message, 'info', duration);
88
+ }
89
+ };
90
+
91
+ // Initialize toast manager
92
+ document.addEventListener('DOMContentLoaded', () => {
93
+ ToastManager.init();
94
+ });
95
+
96
+ // =============================================================================
97
+ // Global State
98
+ // =============================================================================
99
  const AppState = {
100
  currentTab: 'dashboard',
101
  data: {},
102
+ charts: {},
103
+ isLoading: false
104
  };
105
 
106
  // Initialize app
 
168
  function loadTabData(tabId) {
169
  switch(tabId) {
170
  case 'dashboard':
171
+ loadDashboard();
172
+ break;
173
  case 'market':
174
  loadMarketData();
175
  break;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  case 'models':
177
  loadModels();
178
  break;
179
  case 'sentiment':
180
+ loadSentimentModels(); // Populate model dropdown
181
+ loadSentimentHistory(); // Load history from localStorage
182
+ break;
183
+ case 'ai-analyst':
184
+ // AI analyst tab is interactive, no auto-load needed
185
+ break;
186
+ case 'trading-assistant':
187
+ // Trading assistant tab is interactive, no auto-load needed
188
  break;
189
  case 'news':
190
  loadNews();
 
198
  case 'api-explorer':
199
  loadAPIEndpoints();
200
  break;
201
+ default:
202
+ console.log('No specific loader for tab:', tabId);
203
  }
204
  }
205
 
 
223
  { value: '/api/models/list', text: 'GET /api/models/list - List Models' },
224
  { value: '/api/models/status', text: 'GET /api/models/status - Models Status' },
225
  { value: '/api/models/data/stats', text: 'GET /api/models/data/stats - Models Statistics' },
226
+ { value: '/api/analyze/text', text: 'POST /api/analyze/text - AI Text Analysis' },
227
+ { value: '/api/trading/decision', text: 'POST /api/trading/decision - Trading Signal' },
228
+ { value: '/api/sentiment/analyze', text: 'POST /api/sentiment/analyze - Analyze Sentiment' },
229
  { value: '/api/logs/recent', text: 'GET /api/logs/recent - Recent Logs' },
230
  { value: '/api/logs/errors', text: 'GET /api/logs/errors - Error Logs' },
231
  { value: '/api/diagnostics/last', text: 'GET /api/diagnostics/last - Last Diagnostics' },
 
266
 
267
  // Load Dashboard
268
  async function loadDashboard() {
269
+ // Show loading state
270
+ const statsElements = [
271
+ 'stat-total-resources', 'stat-free-resources',
272
+ 'stat-models', 'stat-providers'
273
+ ];
274
+ statsElements.forEach(id => {
275
+ const el = document.getElementById(id);
276
+ if (el) el.textContent = '...';
277
+ });
278
+
279
+ const systemStatusDiv = document.getElementById('system-status');
280
+ if (systemStatusDiv) {
281
+ systemStatusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading system status...</div>';
282
+ }
283
+
284
  try {
285
  // Load resources
286
  const resourcesRes = await fetch('/api/resources');
 
326
  }
327
  } catch (error) {
328
  console.error('Error loading dashboard:', error);
329
+ showError('Failed to load dashboard. Please check the backend is running.');
330
+
331
+ // Show error state
332
+ const systemStatusDiv = document.getElementById('system-status');
333
+ if (systemStatusDiv) {
334
+ systemStatusDiv.innerHTML = '<div class="alert alert-error">Failed to load dashboard data. Please refresh or check backend status.</div>';
335
+ }
336
  }
337
  }
338
 
 
378
 
379
  // Load Market Data
380
  async function loadMarketData() {
381
+ // Show loading states
382
+ const marketDiv = document.getElementById('market-data');
383
+ const trendingDiv = document.getElementById('trending-coins');
384
+ const fgDiv = document.getElementById('fear-greed');
385
+
386
+ if (marketDiv) marketDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading market data...</div>';
387
+ if (trendingDiv) trendingDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading trending coins...</div>';
388
+ if (fgDiv) fgDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading Fear & Greed Index...</div>';
389
+
390
  try {
391
  const response = await fetch('/api/market');
392
  const data = await response.json();
 
507
  }
508
  } catch (error) {
509
  console.error('Error loading market data:', error);
510
+ showError('Failed to load market data. Please check the backend connection.');
511
+
512
+ const marketDiv = document.getElementById('market-data');
513
+ if (marketDiv) {
514
+ marketDiv.innerHTML = '<div class="alert alert-error">Failed to load market data. The backend may be offline or the CoinGecko API may be unavailable.</div>';
515
+ }
516
  }
517
  }
518
 
 
528
 
529
  // Load Models
530
  async function loadModels() {
531
+ // Show loading state
532
+ const modelsListDiv = document.getElementById('models-list');
533
+ const statusDiv = document.getElementById('models-status');
534
+
535
+ if (modelsListDiv) modelsListDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading models...</div>';
536
+ if (statusDiv) statusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading status...</div>';
537
+
538
  try {
539
  const response = await fetch('/api/models/list');
540
  const data = await response.json();
 
652
  }
653
  } catch (error) {
654
  console.error('Error loading models:', error);
655
+ showError('Failed to load models. Please check the backend connection.');
656
+
657
+ const modelsListDiv = document.getElementById('models-list');
658
+ if (modelsListDiv) {
659
+ modelsListDiv.innerHTML = '<div class="alert alert-error">Failed to load models. Check backend status.</div>';
660
+ }
661
  }
662
  }
663
 
 
678
  }
679
  }
680
 
681
+ // Load Sentiment Models - updated to populate dropdown for sentiment analysis
682
  async function loadSentimentModels() {
683
  try {
684
  const response = await fetch('/api/models/list');
 
686
 
687
  const models = data.models || data || [];
688
  const select = document.getElementById('sentiment-model');
689
+ if (!select) return;
690
 
691
+ select.innerHTML = '<option value="">Auto (Mode-based)</option>';
692
+
693
+ // Filter and add models - only sentiment and generation models
694
  models.filter(m => {
695
+ const category = m.category || '';
696
+ const task = m.task || '';
697
+ // Include sentiment models and generation/trading models
698
+ return category.includes('sentiment') ||
699
+ category.includes('generation') ||
700
+ category.includes('trading') ||
701
+ task.includes('classification') ||
702
+ task.includes('generation');
703
  }).forEach(model => {
704
  const option = document.createElement('option');
705
+ const modelKey = model.key || model.id;
706
+ const modelName = model.model_id || model.name || modelKey;
707
+ const desc = model.description || model.category || '';
708
+
709
+ option.value = modelKey;
710
+ // Show model name with short description
711
+ const displayName = modelName.length > 40 ? modelName.substring(0, 37) + '...' : modelName;
712
+ option.textContent = displayName;
713
+ option.title = desc; // Full description on hover
714
  select.appendChild(option);
715
  });
716
 
 
718
  if (select.options.length === 1) {
719
  const option = document.createElement('option');
720
  option.value = '';
721
+ option.textContent = 'No models available - will use fallback';
722
  option.disabled = true;
723
  select.appendChild(option);
724
  }
725
+
726
+ console.log(`Loaded ${select.options.length - 1} sentiment models into dropdown`);
727
  } catch (error) {
728
  console.error('Error loading sentiment models:', error);
729
  const select = document.getElementById('sentiment-model');
730
+ if (select) {
731
+ select.innerHTML = '<option value="">Auto (Mode-based)</option>';
732
+ }
733
  }
734
  }
735
 
 
936
  }
937
  }
938
 
939
+ // Summarize News
940
+ async function summarizeNews() {
941
+ const title = document.getElementById('summary-news-title').value.trim();
942
+ const content = document.getElementById('summary-news-content').value.trim();
943
+
944
+ if (!title && !content) {
945
+ showError('Please enter news title or content');
946
+ return;
947
+ }
948
+
949
+ const resultDiv = document.getElementById('news-summary-result');
950
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Generating summary...</div>';
951
+
952
+ try {
953
+ const response = await fetch('/api/news/summarize', {
954
+ method: 'POST',
955
+ headers: { 'Content-Type': 'application/json' },
956
+ body: JSON.stringify({ title: title, content: content })
957
+ });
958
+
959
+ const data = await response.json();
960
+
961
+ if (!data.success) {
962
+ resultDiv.innerHTML = `
963
+ <div class="alert alert-error">
964
+ <strong>❌ Summarization Failed:</strong> ${data.error || 'Failed to generate summary'}
965
+ </div>
966
+ `;
967
+ return;
968
+ }
969
+
970
+ const summary = data.summary || '';
971
+ const model = data.model || 'Unknown';
972
+ const isHFModel = data.available !== false && model !== 'fallback_extractive';
973
+ const modelDisplay = isHFModel ? model : `${model} (Fallback)`;
974
+
975
+ // Create collapsible card with summary
976
+ resultDiv.innerHTML = `
977
+ <div class="alert alert-success" style="border-left: 4px solid var(--primary);">
978
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
979
+ <h4 style="margin: 0;">📝 News Summary</h4>
980
+ <button class="btn-secondary" onclick="toggleSummaryDetails()" style="padding: 5px 10px; font-size: 12px;">
981
+ <span id="toggle-summary-icon">▼</span> Details
982
+ </button>
983
+ </div>
984
+
985
+ ${title ? `<div style="margin-bottom: 10px;">
986
+ <strong>Title:</strong>
987
+ <span style="color: var(--text-primary);">${title}</span>
988
+ </div>` : ''}
989
+
990
+ <div style="background: var(--bg-card); padding: 15px; border-radius: 8px; margin: 15px 0;">
991
+ <strong style="color: var(--primary);">Summary:</strong>
992
+ <p style="margin-top: 10px; line-height: 1.6; color: var(--text-primary);">
993
+ ${summary}
994
+ </p>
995
+ </div>
996
+
997
+ <div id="summary-details" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
998
+ <div style="display: grid; gap: 10px;">
999
+ <div>
1000
+ <strong>Model:</strong>
1001
+ <span style="color: var(--text-secondary);">${modelDisplay}</span>
1002
+ ${!isHFModel ? '<span style="color: var(--warning); font-size: 12px; margin-left: 5px;">⚠️ HF model unavailable</span>' : ''}
1003
+ </div>
1004
+ ${data.input_length ? `<div>
1005
+ <strong>Input Length:</strong>
1006
+ <span style="color: var(--text-secondary);">${data.input_length} characters</span>
1007
+ </div>` : ''}
1008
+ <div>
1009
+ <strong>Timestamp:</strong>
1010
+ <span style="color: var(--text-secondary);">${new Date(data.timestamp).toLocaleString()}</span>
1011
+ </div>
1012
+ ${data.note ? `<div style="color: var(--warning); font-size: 13px;">
1013
+ <strong>Note:</strong> ${data.note}
1014
+ </div>` : ''}
1015
+ </div>
1016
+ </div>
1017
+
1018
+ <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
1019
+ <button class="btn-primary" onclick="copySummaryToClipboard()" style="margin-right: 10px;">
1020
+ 📋 Copy Summary
1021
+ </button>
1022
+ <button class="btn-secondary" onclick="clearSummaryForm()">
1023
+ 🔄 Clear
1024
+ </button>
1025
+ </div>
1026
+ </div>
1027
+ `;
1028
+
1029
+ // Store summary for clipboard
1030
+ window.lastSummary = summary;
1031
+
1032
+ } catch (error) {
1033
+ console.error('News summarization error:', error);
1034
+ resultDiv.innerHTML = `<div class="alert alert-error">Summarization Error: ${error.message}</div>`;
1035
+ showError('Error summarizing news');
1036
+ }
1037
+ }
1038
+
1039
+ // Toggle summary details
1040
+ function toggleSummaryDetails() {
1041
+ const details = document.getElementById('summary-details');
1042
+ const icon = document.getElementById('toggle-summary-icon');
1043
+ if (details.style.display === 'none') {
1044
+ details.style.display = 'block';
1045
+ icon.textContent = '▲';
1046
+ } else {
1047
+ details.style.display = 'none';
1048
+ icon.textContent = '▼';
1049
+ }
1050
+ }
1051
+
1052
+ // Copy summary to clipboard
1053
+ async function copySummaryToClipboard() {
1054
+ if (!window.lastSummary) {
1055
+ showError('No summary to copy');
1056
+ return;
1057
+ }
1058
+
1059
+ try {
1060
+ await navigator.clipboard.writeText(window.lastSummary);
1061
+ showSuccess('Summary copied to clipboard!');
1062
+ } catch (error) {
1063
+ console.error('Failed to copy:', error);
1064
+ showError('Failed to copy summary');
1065
+ }
1066
+ }
1067
+
1068
+ // Clear summary form
1069
+ function clearSummaryForm() {
1070
+ document.getElementById('summary-news-title').value = '';
1071
+ document.getElementById('summary-news-content').value = '';
1072
+ document.getElementById('news-summary-result').innerHTML = '';
1073
+ window.lastSummary = null;
1074
+ }
1075
+
1076
+ // Analyze Sentiment (updated with model_key support)
1077
  async function analyzeSentiment() {
1078
  const text = document.getElementById('sentiment-text').value;
1079
  const mode = document.getElementById('sentiment-mode').value;
 
1090
  try {
1091
  let response;
1092
 
1093
+ // Build request body
1094
+ const requestBody = {
1095
+ text: text,
1096
+ mode: mode
1097
+ };
1098
+
1099
+ // Add model_key if specific model selected
1100
+ if (modelKey && modelKey !== '') {
1101
+ requestBody.model_key = modelKey;
1102
+ }
1103
+
1104
+ // Use the sentiment endpoint with mode and optional model_key
1105
+ response = await fetch('/api/sentiment', {
1106
  method: 'POST',
1107
  headers: { 'Content-Type': 'application/json' },
1108
+ body: JSON.stringify(requestBody)
1109
  });
1110
 
1111
  const data = await response.json();
 
1230
 
1231
  // Load News
1232
  async function loadNews() {
1233
+ // Show loading state
1234
+ const newsDiv = document.getElementById('news-list');
1235
+ if (newsDiv) {
1236
+ newsDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading news...</div>';
1237
+ }
1238
+
1239
  try {
1240
  // Try /api/news/latest first, fallback to /api/news
1241
  let response;
 
1400
 
1401
  // Load Providers
1402
  async function loadProviders() {
1403
+ // Show loading state
1404
+ const providersDiv = document.getElementById('providers-list');
1405
+ if (providersDiv) {
1406
+ providersDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading providers...</div>';
1407
+ }
1408
+
1409
  try {
1410
  // Load providers and auto-discovery health summary in parallel
1411
  const [providersRes, healthRes] = await Promise.all([
 
1713
  }
1714
  }
1715
 
1716
+ // Load Health Diagnostics
1717
+ async function loadHealthDiagnostics() {
1718
+ const resultDiv = document.getElementById('health-diagnostics-result');
1719
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading health data...</div>';
1720
+
1721
+ try {
1722
+ const response = await fetch('/api/diagnostics/health');
1723
+ const data = await response.json();
1724
+
1725
+ if (data.status !== 'success') {
1726
+ resultDiv.innerHTML = `
1727
+ <div class="alert alert-error">
1728
+ <strong>Error:</strong> ${data.error || 'Failed to load health diagnostics'}
1729
+ </div>
1730
+ `;
1731
+ return;
1732
+ }
1733
+
1734
+ const providerSummary = data.providers.summary;
1735
+ const modelSummary = data.models.summary;
1736
+ const providerEntries = data.providers.entries || [];
1737
+ const modelEntries = data.models.entries || [];
1738
+
1739
+ // Helper function to get status color
1740
+ const getStatusColor = (status) => {
1741
+ switch (status) {
1742
+ case 'healthy': return 'var(--success)';
1743
+ case 'degraded': return 'var(--warning)';
1744
+ case 'unavailable': return 'var(--danger)';
1745
+ default: return 'var(--text-secondary)';
1746
+ }
1747
+ };
1748
+
1749
+ // Helper function to get status badge
1750
+ const getStatusBadge = (status, inCooldown) => {
1751
+ const color = getStatusColor(status);
1752
+ const icon = status === 'healthy' ? '✅' :
1753
+ status === 'degraded' ? '⚠️' :
1754
+ status === 'unavailable' ? '❌' : '❓';
1755
+ const cooldownText = inCooldown ? ' (cooldown)' : '';
1756
+ return `<span style="padding: 4px 10px; background: ${color}20; color: ${color}; border-radius: 5px; font-size: 12px; font-weight: 600;">${icon} ${status}${cooldownText}</span>`;
1757
+ };
1758
+
1759
+ resultDiv.innerHTML = `
1760
+ <div style="display: grid; gap: 20px;">
1761
+ <!-- Summary Cards -->
1762
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
1763
+ <div style="padding: 15px; background: rgba(59, 130, 246, 0.1); border-radius: 10px; border-left: 4px solid var(--accent-blue);">
1764
+ <div style="font-size: 24px; font-weight: 800; color: var(--accent-blue); margin-bottom: 5px;">
1765
+ ${providerSummary.total}
1766
+ </div>
1767
+ <div style="font-size: 12px; color: var(--text-secondary);">Total Providers</div>
1768
+ <div style="margin-top: 8px; display: flex; gap: 8px; font-size: 11px;">
1769
+ <span style="color: var(--success);">✅ ${providerSummary.healthy}</span>
1770
+ <span style="color: var(--warning);">⚠️ ${providerSummary.degraded}</span>
1771
+ <span style="color: var(--danger);">❌ ${providerSummary.unavailable}</span>
1772
+ </div>
1773
+ </div>
1774
+
1775
+ <div style="padding: 15px; background: rgba(139, 92, 246, 0.1); border-radius: 10px; border-left: 4px solid var(--accent-purple);">
1776
+ <div style="font-size: 24px; font-weight: 800; color: var(--accent-purple); margin-bottom: 5px;">
1777
+ ${modelSummary.total}
1778
+ </div>
1779
+ <div style="font-size: 12px; color: var(--text-secondary);">Total Models</div>
1780
+ <div style="margin-top: 8px; display: flex; gap: 8px; font-size: 11px;">
1781
+ <span style="color: var(--success);">✅ ${modelSummary.healthy}</span>
1782
+ <span style="color: var(--warning);">⚠️ ${modelSummary.degraded}</span>
1783
+ <span style="color: var(--danger);">❌ ${modelSummary.unavailable}</span>
1784
+ </div>
1785
+ </div>
1786
+
1787
+ <div style="padding: 15px; background: ${data.overall_health.providers_ok && data.overall_health.models_ok ? 'rgba(16, 185, 129, 0.1)' : 'rgba(245, 158, 11, 0.1)'}; border-radius: 10px; border-left: 4px solid ${data.overall_health.providers_ok && data.overall_health.models_ok ? 'var(--success)' : 'var(--warning)'};">
1788
+ <div style="font-size: 32px; margin-bottom: 5px;">
1789
+ ${data.overall_health.providers_ok && data.overall_health.models_ok ? '💚' : '⚠️'}
1790
+ </div>
1791
+ <div style="font-size: 12px; color: var(--text-secondary);">Overall Health</div>
1792
+ <div style="margin-top: 8px; font-size: 14px; font-weight: 600; color: ${data.overall_health.providers_ok && data.overall_health.models_ok ? 'var(--success)' : 'var(--warning)'};">
1793
+ ${data.overall_health.providers_ok && data.overall_health.models_ok ? 'HEALTHY' : 'DEGRADED'}
1794
+ </div>
1795
+ </div>
1796
+ </div>
1797
+
1798
+ <!-- Providers Health -->
1799
+ ${providerEntries.length > 0 ? `
1800
+ <div>
1801
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
1802
+ <h4 style="margin: 0; color: var(--text-primary);">🔌 Provider Health (${providerEntries.length})</h4>
1803
+ </div>
1804
+ <div style="display: grid; gap: 10px; max-height: 300px; overflow-y: auto;">
1805
+ ${providerEntries.map(provider => `
1806
+ <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; border-left: 3px solid ${getStatusColor(provider.status)};">
1807
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
1808
+ <div style="font-weight: 600; color: var(--text-primary);">${provider.name}</div>
1809
+ ${getStatusBadge(provider.status, provider.in_cooldown)}
1810
+ </div>
1811
+ <div style="font-size: 11px; color: var(--text-secondary); display: grid; gap: 3px;">
1812
+ <div>Errors: ${provider.error_count} | Successes: ${provider.success_count}</div>
1813
+ ${provider.last_success ? `<div>Last Success: ${new Date(provider.last_success * 1000).toLocaleString()}</div>` : ''}
1814
+ ${provider.last_error ? `<div>Last Error: ${new Date(provider.last_error * 1000).toLocaleString()}</div>` : ''}
1815
+ ${provider.last_error_message ? `<div style="color: var(--danger); margin-top: 5px;">Error: ${provider.last_error_message.substring(0, 100)}${provider.last_error_message.length > 100 ? '...' : ''}</div>` : ''}
1816
+ </div>
1817
+ </div>
1818
+ `).join('')}
1819
+ </div>
1820
+ </div>
1821
+ ` : '<div class="alert alert-info">No provider health data available yet</div>'}
1822
+
1823
+ <!-- Models Health -->
1824
+ ${modelEntries.length > 0 ? `
1825
+ <div>
1826
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
1827
+ <h4 style="margin: 0; color: var(--text-primary);">🤖 Model Health (${modelEntries.length})</h4>
1828
+ <button class="btn-secondary" onclick="triggerSelfHeal()" style="padding: 6px 12px; font-size: 12px;">
1829
+ 🔧 Auto-Heal Failed Models
1830
+ </button>
1831
+ </div>
1832
+ <div style="display: grid; gap: 10px; max-height: 400px; overflow-y: auto;">
1833
+ ${modelEntries.filter(m => m.loaded || m.status !== 'unknown').slice(0, 20).map(model => `
1834
+ <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; border-left: 3px solid ${getStatusColor(model.status)};">
1835
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px; gap: 10px;">
1836
+ <div>
1837
+ <div style="font-weight: 600; color: var(--text-primary); margin-bottom: 3px;">${model.model_id}</div>
1838
+ <div style="font-size: 10px; color: var(--text-secondary);">${model.key} • ${model.category}</div>
1839
+ </div>
1840
+ <div style="text-align: right; white-space: nowrap;">
1841
+ ${getStatusBadge(model.status, model.in_cooldown)}
1842
+ ${model.status === 'unavailable' && !model.in_cooldown ? `<button class="btn-secondary" onclick="reinitModel('${model.key}')" style="padding: 4px 8px; font-size: 10px; margin-top: 5px;">Reinit</button>` : ''}
1843
+ </div>
1844
+ </div>
1845
+ <div style="font-size: 11px; color: var(--text-secondary); display: grid; gap: 3px;">
1846
+ <div>Errors: ${model.error_count} | Successes: ${model.success_count} | Loaded: ${model.loaded ? 'Yes' : 'No'}</div>
1847
+ ${model.last_success ? `<div>Last Success: ${new Date(model.last_success * 1000).toLocaleString()}</div>` : ''}
1848
+ ${model.last_error ? `<div>Last Error: ${new Date(model.last_error * 1000).toLocaleString()}</div>` : ''}
1849
+ ${model.last_error_message ? `<div style="color: var(--danger); margin-top: 5px;">Error: ${model.last_error_message.substring(0, 150)}${model.last_error_message.length > 150 ? '...' : ''}</div>` : ''}
1850
+ </div>
1851
+ </div>
1852
+ `).join('')}
1853
+ </div>
1854
+ </div>
1855
+ ` : '<div class="alert alert-info">No model health data available yet</div>'}
1856
+
1857
+ <div style="text-align: center; padding: 15px; background: rgba(31, 41, 55, 0.3); border-radius: 8px; font-size: 11px; color: var(--text-secondary);">
1858
+ Last updated: ${new Date(data.timestamp).toLocaleString()}
1859
+ </div>
1860
+ </div>
1861
+ `;
1862
+
1863
+ } catch (error) {
1864
+ console.error('Error loading health diagnostics:', error);
1865
+ resultDiv.innerHTML = `
1866
+ <div class="alert alert-error">
1867
+ <strong>Error:</strong> ${error.message || 'Failed to load health diagnostics'}
1868
+ </div>
1869
+ `;
1870
+ }
1871
+ }
1872
+
1873
+ // Trigger self-heal for all failed models
1874
+ async function triggerSelfHeal() {
1875
+ try {
1876
+ const response = await fetch('/api/diagnostics/self-heal', { method: 'POST' });
1877
+ const data = await response.json();
1878
+
1879
+ if (data.status === 'completed') {
1880
+ const summary = data.summary;
1881
+ showSuccess(`Self-heal completed: ${summary.successful}/${summary.total_attempts} successful`);
1882
+ // Reload health after a short delay
1883
+ setTimeout(loadHealthDiagnostics, 2000);
1884
+ } else {
1885
+ showError(data.error || 'Self-heal failed');
1886
+ }
1887
+ } catch (error) {
1888
+ showError('Error triggering self-heal: ' + error.message);
1889
+ }
1890
+ }
1891
+
1892
+ // Reinitialize specific model
1893
+ async function reinitModel(modelKey) {
1894
+ try {
1895
+ const response = await fetch(`/api/diagnostics/self-heal?model_key=${encodeURIComponent(modelKey)}`, {
1896
+ method: 'POST'
1897
+ });
1898
+ const data = await response.json();
1899
+
1900
+ if (data.status === 'completed' && data.results && data.results.length > 0) {
1901
+ const result = data.results[0];
1902
+ if (result.status === 'success') {
1903
+ showSuccess(`Model ${modelKey} reinitialized successfully`);
1904
+ } else {
1905
+ showError(`Failed to reinit ${modelKey}: ${result.message || result.error || 'Unknown error'}`);
1906
+ }
1907
+ // Reload health after a short delay
1908
+ setTimeout(loadHealthDiagnostics, 1500);
1909
+ } else {
1910
+ showError(data.error || 'Reinitialization failed');
1911
+ }
1912
+ } catch (error) {
1913
+ showError('Error reinitializing model: ' + error.message);
1914
+ }
1915
+ }
1916
+
1917
  // Test API
1918
  async function testAPI() {
1919
  const endpoint = document.getElementById('api-endpoint').value;
 
1998
 
1999
  // Utility Functions
2000
  function showError(message) {
2001
+ console.error(message);
2002
+ ToastManager.error(message);
 
 
 
2003
  }
2004
 
2005
  function showSuccess(message) {
2006
+ console.log(message);
2007
+ ToastManager.success(message);
 
 
 
2008
  }
2009
 
2010
  // Additional tab loaders for HTML tabs
 
2341
  await originalLoadDashboard();
2342
  updateHeaderStats();
2343
  };
2344
+
2345
+ // ===== AI Analyst Functions =====
2346
+ async function runAIAnalyst() {
2347
+ const prompt = document.getElementById('ai-analyst-prompt').value.trim();
2348
+ const mode = document.getElementById('ai-analyst-mode').value;
2349
+ const maxLength = parseInt(document.getElementById('ai-analyst-max-length').value);
2350
+
2351
+ if (!prompt) {
2352
+ showError('Please enter a prompt or question');
2353
+ return;
2354
+ }
2355
+
2356
+ const resultDiv = document.getElementById('ai-analyst-result');
2357
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Generating analysis...</div>';
2358
+
2359
+ try {
2360
+ const response = await fetch('/api/analyze/text', {
2361
+ method: 'POST',
2362
+ headers: { 'Content-Type': 'application/json' },
2363
+ body: JSON.stringify({
2364
+ prompt: prompt,
2365
+ mode: mode,
2366
+ max_length: maxLength
2367
+ })
2368
+ });
2369
+
2370
+ const data = await response.json();
2371
+
2372
+ if (!data.available) {
2373
+ resultDiv.innerHTML = `
2374
+ <div class="alert alert-warning">
2375
+ <strong>⚠️ Model Not Available:</strong> ${data.error || 'AI generation model is currently unavailable'}
2376
+ ${data.note ? `<br><small>${data.note}</small>` : ''}
2377
+ </div>
2378
+ `;
2379
+ return;
2380
+ }
2381
+
2382
+ if (!data.success) {
2383
+ resultDiv.innerHTML = `
2384
+ <div class="alert alert-error">
2385
+ <strong>❌ Generation Failed:</strong> ${data.error || 'Failed to generate analysis'}
2386
+ </div>
2387
+ `;
2388
+ return;
2389
+ }
2390
+
2391
+ const generatedText = data.text || '';
2392
+ const model = data.model || 'Unknown';
2393
+
2394
+ resultDiv.innerHTML = `
2395
+ <div class="alert alert-success" style="border-left: 4px solid var(--primary);">
2396
+ <div style="display: flex; justify-content: between; align-items: center; margin-bottom: 15px;">
2397
+ <h4 style="margin: 0;">✨ AI Generated Analysis</h4>
2398
+ </div>
2399
+
2400
+ <div style="background: var(--bg-card); padding: 20px; border-radius: 8px; margin: 15px 0;">
2401
+ <div style="line-height: 1.8; color: var(--text-primary); white-space: pre-wrap;">
2402
+ ${generatedText}
2403
+ </div>
2404
+ </div>
2405
+
2406
+ <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
2407
+ <div style="display: grid; gap: 10px; font-size: 13px;">
2408
+ <div>
2409
+ <strong>Model:</strong>
2410
+ <span style="color: var(--text-secondary);">${model}</span>
2411
+ </div>
2412
+ <div>
2413
+ <strong>Mode:</strong>
2414
+ <span style="color: var(--text-secondary);">${mode}</span>
2415
+ </div>
2416
+ <div>
2417
+ <strong>Prompt:</strong>
2418
+ <span style="color: var(--text-secondary);">"${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}"</span>
2419
+ </div>
2420
+ <div>
2421
+ <strong>Timestamp:</strong>
2422
+ <span style="color: var(--text-secondary);">${new Date(data.timestamp).toLocaleString()}</span>
2423
+ </div>
2424
+ </div>
2425
+ </div>
2426
+
2427
+ <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
2428
+ <button class="btn-primary" onclick="copyAIAnalystResult()" style="margin-right: 10px;">
2429
+ 📋 Copy Analysis
2430
+ </button>
2431
+ <button class="btn-secondary" onclick="clearAIAnalystForm()">
2432
+ 🔄 Clear
2433
+ </button>
2434
+ </div>
2435
+ </div>
2436
+ `;
2437
+
2438
+ // Store for clipboard
2439
+ window.lastAIAnalysis = generatedText;
2440
+
2441
+ } catch (error) {
2442
+ console.error('AI analyst error:', error);
2443
+ resultDiv.innerHTML = `<div class="alert alert-error">Generation Error: ${error.message}</div>`;
2444
+ showError('Error generating analysis');
2445
+ }
2446
+ }
2447
+
2448
+ function setAIAnalystPrompt(text) {
2449
+ document.getElementById('ai-analyst-prompt').value = text;
2450
+ }
2451
+
2452
+ async function copyAIAnalystResult() {
2453
+ if (!window.lastAIAnalysis) {
2454
+ showError('No analysis to copy');
2455
+ return;
2456
+ }
2457
+
2458
+ try {
2459
+ await navigator.clipboard.writeText(window.lastAIAnalysis);
2460
+ showSuccess('Analysis copied to clipboard!');
2461
+ } catch (error) {
2462
+ console.error('Failed to copy:', error);
2463
+ showError('Failed to copy analysis');
2464
+ }
2465
+ }
2466
+
2467
+ function clearAIAnalystForm() {
2468
+ document.getElementById('ai-analyst-prompt').value = '';
2469
+ document.getElementById('ai-analyst-result').innerHTML = '';
2470
+ window.lastAIAnalysis = null;
2471
+ }
2472
+
2473
+ // ===== Trading Assistant Functions =====
2474
+ async function runTradingAssistant() {
2475
+ const symbol = document.getElementById('trading-symbol').value.trim().toUpperCase();
2476
+ const context = document.getElementById('trading-context').value.trim();
2477
+
2478
+ if (!symbol) {
2479
+ showError('Please enter a trading symbol');
2480
+ return;
2481
+ }
2482
+
2483
+ const resultDiv = document.getElementById('trading-assistant-result');
2484
+ resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing and generating trading signal...</div>';
2485
+
2486
+ try {
2487
+ const response = await fetch('/api/trading/decision', {
2488
+ method: 'POST',
2489
+ headers: { 'Content-Type': 'application/json' },
2490
+ body: JSON.stringify({
2491
+ symbol: symbol,
2492
+ context: context
2493
+ })
2494
+ });
2495
+
2496
+ const data = await response.json();
2497
+
2498
+ if (!data.available) {
2499
+ resultDiv.innerHTML = `
2500
+ <div class="alert alert-warning">
2501
+ <strong>⚠️ Model Not Available:</strong> ${data.error || 'Trading signal model is currently unavailable'}
2502
+ ${data.note ? `<br><small>${data.note}</small>` : ''}
2503
+ </div>
2504
+ `;
2505
+ return;
2506
+ }
2507
+
2508
+ if (!data.success) {
2509
+ resultDiv.innerHTML = `
2510
+ <div class="alert alert-error">
2511
+ <strong>❌ Analysis Failed:</strong> ${data.error || 'Failed to generate trading signal'}
2512
+ </div>
2513
+ `;
2514
+ return;
2515
+ }
2516
+
2517
+ const decision = data.decision || 'HOLD';
2518
+ const confidence = data.confidence || 0;
2519
+ const rationale = data.rationale || '';
2520
+ const model = data.model || 'Unknown';
2521
+
2522
+ // Determine colors and icons based on decision
2523
+ let decisionColor, decisionBg, decisionIcon;
2524
+ if (decision === 'BUY') {
2525
+ decisionColor = 'var(--success)';
2526
+ decisionBg = 'rgba(16, 185, 129, 0.2)';
2527
+ decisionIcon = '📈';
2528
+ } else if (decision === 'SELL') {
2529
+ decisionColor = 'var(--danger)';
2530
+ decisionBg = 'rgba(239, 68, 68, 0.2)';
2531
+ decisionIcon = '📉';
2532
+ } else {
2533
+ decisionColor = 'var(--text-secondary)';
2534
+ decisionBg = 'rgba(156, 163, 175, 0.2)';
2535
+ decisionIcon = '➡️';
2536
+ }
2537
+
2538
+ resultDiv.innerHTML = `
2539
+ <div class="alert alert-success" style="border-left: 4px solid ${decisionColor};">
2540
+ <h4 style="margin-bottom: 20px;">🎯 Trading Signal for ${symbol}</h4>
2541
+
2542
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
2543
+ <div style="text-align: center; padding: 30px; background: ${decisionBg}; border-radius: 10px;">
2544
+ <div style="font-size: 48px; margin-bottom: 10px;">${decisionIcon}</div>
2545
+ <div style="font-size: 32px; font-weight: 800; color: ${decisionColor}; margin-bottom: 5px;">
2546
+ ${decision}
2547
+ </div>
2548
+ <div style="font-size: 14px; color: var(--text-secondary);">
2549
+ Decision
2550
+ </div>
2551
+ </div>
2552
+
2553
+ <div style="text-align: center; padding: 30px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
2554
+ <div style="font-size: 48px; font-weight: 800; color: var(--primary); margin-bottom: 10px;">
2555
+ ${(confidence * 100).toFixed(0)}%
2556
+ </div>
2557
+ <div style="font-size: 14px; color: var(--text-secondary);">
2558
+ Confidence
2559
+ </div>
2560
+ </div>
2561
+ </div>
2562
+
2563
+ <div style="background: var(--bg-card); padding: 20px; border-radius: 8px; margin: 20px 0;">
2564
+ <strong style="color: var(--primary);">AI Rationale:</strong>
2565
+ <p style="margin-top: 10px; line-height: 1.6; color: var(--text-primary); white-space: pre-wrap;">
2566
+ ${rationale}
2567
+ </p>
2568
+ </div>
2569
+
2570
+ ${context ? `
2571
+ <div style="margin-top: 15px; padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 8px;">
2572
+ <strong>Your Context:</strong>
2573
+ <div style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
2574
+ "${context.substring(0, 200)}${context.length > 200 ? '...' : ''}"
2575
+ </div>
2576
+ </div>
2577
+ ` : ''}
2578
+
2579
+ <div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--border);">
2580
+ <div style="display: grid; gap: 10px; font-size: 13px;">
2581
+ <div>
2582
+ <strong>Model:</strong>
2583
+ <span style="color: var(--text-secondary);">${model}</span>
2584
+ </div>
2585
+ <div>
2586
+ <strong>Timestamp:</strong>
2587
+ <span style="color: var(--text-secondary);">${new Date(data.timestamp).toLocaleString()}</span>
2588
+ </div>
2589
+ </div>
2590
+ </div>
2591
+
2592
+ <div style="margin-top: 20px; padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 8px; border-left: 3px solid var(--warning);">
2593
+ <strong style="color: var(--warning);">⚠️ Reminder:</strong>
2594
+ <p style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
2595
+ This is an AI-generated signal for informational purposes only. Always do your own research and consider multiple factors before trading.
2596
+ </p>
2597
+ </div>
2598
+ </div>
2599
+ `;
2600
+
2601
+ } catch (error) {
2602
+ console.error('Trading assistant error:', error);
2603
+ resultDiv.innerHTML = `<div class="alert alert-error">Analysis Error: ${error.message}</div>`;
2604
+ showError('Error generating trading signal');
2605
+ }
2606
+ }
2607
+
2608
+ // Initialize trading pair selector for trading assistant tab
2609
+ function initTradingSymbolSelector() {
2610
+ const tradingSymbolContainer = document.getElementById('trading-symbol-container');
2611
+ if (tradingSymbolContainer && window.TradingPairsLoader) {
2612
+ const pairs = window.TradingPairsLoader.getTradingPairs();
2613
+ if (pairs && pairs.length > 0) {
2614
+ tradingSymbolContainer.innerHTML = window.TradingPairsLoader.createTradingPairCombobox(
2615
+ 'trading-symbol',
2616
+ 'Select or type trading pair',
2617
+ 'BTCUSDT'
2618
+ );
2619
+ }
2620
+ }
2621
+ }
2622
+
2623
+ // Update loadTabData to handle new tabs
2624
+ const originalLoadTabData = loadTabData;
2625
+ loadTabData = function(tabId) {
2626
+ originalLoadTabData(tabId);
2627
+
2628
+ // Additional handlers for new tabs
2629
+ if (tabId === 'ai-analyst') {
2630
+ // No initialization needed for AI Analyst yet
2631
+ } else if (tabId === 'trading-assistant') {
2632
+ initTradingSymbolSelector();
2633
+ }
2634
+ };
2635
+
2636
+ // Listen for trading pairs loaded event to initialize trading symbol selector
2637
+ document.addEventListener('tradingPairsLoaded', function(e) {
2638
+ initTradingSymbolSelector();
2639
+ });
static/js/chartLabView.js CHANGED
@@ -1,458 +1,127 @@
1
  import apiClient from './apiClient.js';
2
- import errorHelper from './errorHelper.js';
3
- import { createAdvancedLineChart, createCandlestickChart, createVolumeChart } from './tradingview-charts.js';
4
-
5
- // Cryptocurrency symbols list
6
- const CRYPTO_SYMBOLS = [
7
- { symbol: 'BTC', name: 'Bitcoin' },
8
- { symbol: 'ETH', name: 'Ethereum' },
9
- { symbol: 'BNB', name: 'Binance Coin' },
10
- { symbol: 'SOL', name: 'Solana' },
11
- { symbol: 'XRP', name: 'Ripple' },
12
- { symbol: 'ADA', name: 'Cardano' },
13
- { symbol: 'DOGE', name: 'Dogecoin' },
14
- { symbol: 'DOT', name: 'Polkadot' },
15
- { symbol: 'MATIC', name: 'Polygon' },
16
- { symbol: 'AVAX', name: 'Avalanche' },
17
- { symbol: 'LINK', name: 'Chainlink' },
18
- { symbol: 'UNI', name: 'Uniswap' },
19
- { symbol: 'LTC', name: 'Litecoin' },
20
- { symbol: 'ATOM', name: 'Cosmos' },
21
- { symbol: 'ALGO', name: 'Algorand' },
22
- { symbol: 'TRX', name: 'Tron' },
23
- { symbol: 'XLM', name: 'Stellar' },
24
- { symbol: 'VET', name: 'VeChain' },
25
- { symbol: 'FIL', name: 'Filecoin' },
26
- { symbol: 'ETC', name: 'Ethereum Classic' },
27
- { symbol: 'AAVE', name: 'Aave' },
28
- { symbol: 'MKR', name: 'Maker' },
29
- { symbol: 'COMP', name: 'Compound' },
30
- { symbol: 'SUSHI', name: 'SushiSwap' },
31
- { symbol: 'YFI', name: 'Yearn Finance' },
32
- ];
33
 
34
  class ChartLabView {
35
  constructor(section) {
36
  this.section = section;
37
- this.symbolInput = section.querySelector('[data-chart-symbol-input]');
38
- this.symbolDropdown = section.querySelector('[data-chart-symbol-dropdown]');
39
- this.symbolOptions = section.querySelector('[data-chart-symbol-options]');
40
- this.timeframeButtons = section.querySelectorAll('[data-timeframe]');
41
- this.indicatorButtons = section.querySelectorAll('[data-indicator]');
42
- this.loadButton = section.querySelector('[data-load-chart]');
43
- this.runAnalysisButton = section.querySelector('[data-run-analysis]');
44
- this.canvas = section.querySelector('#price-chart');
45
- this.analysisOutput = section.querySelector('[data-analysis-output]');
46
- this.chartTitle = section.querySelector('[data-chart-title]');
47
- this.chartLegend = section.querySelector('[data-chart-legend]');
48
  this.chart = null;
49
  this.symbol = 'BTC';
50
  this.timeframe = '7d';
51
- this.filteredSymbols = [...CRYPTO_SYMBOLS];
52
  }
53
 
54
  async init() {
55
- this.setupCombobox();
56
- this.bindEvents();
57
  await this.loadChart();
58
- }
59
-
60
- setupCombobox() {
61
- if (!this.symbolInput || !this.symbolOptions) return;
62
-
63
- // Populate options
64
- this.renderOptions();
65
-
66
- // Set initial value
67
- this.symbolInput.value = 'BTC - Bitcoin';
68
-
69
- // Input event for filtering
70
- this.symbolInput.addEventListener('input', (e) => {
71
- const query = e.target.value.trim().toUpperCase();
72
- this.filterSymbols(query);
73
- });
74
-
75
- // Focus event to show dropdown
76
- this.symbolInput.addEventListener('focus', () => {
77
- this.symbolDropdown.style.display = 'block';
78
- this.filterSymbols(this.symbolInput.value.trim().toUpperCase());
79
- });
80
-
81
- // Click outside to close
82
- document.addEventListener('click', (e) => {
83
- if (!this.symbolInput.contains(e.target) && !this.symbolDropdown.contains(e.target)) {
84
- this.symbolDropdown.style.display = 'none';
85
- }
86
- });
87
- }
88
-
89
- filterSymbols(query) {
90
- if (!query) {
91
- this.filteredSymbols = [...CRYPTO_SYMBOLS];
92
- } else {
93
- this.filteredSymbols = CRYPTO_SYMBOLS.filter(item =>
94
- item.symbol.includes(query) ||
95
- item.name.toUpperCase().includes(query)
96
- );
97
- }
98
- this.renderOptions();
99
- }
100
-
101
- renderOptions() {
102
- if (!this.symbolOptions) return;
103
-
104
- if (this.filteredSymbols.length === 0) {
105
- this.symbolOptions.innerHTML = '<div class="combobox-option disabled">No results found</div>';
106
- return;
107
- }
108
-
109
- this.symbolOptions.innerHTML = this.filteredSymbols.map(item => `
110
- <div class="combobox-option" data-symbol="${item.symbol}">
111
- <strong>${item.symbol}</strong>
112
- <span>${item.name}</span>
113
- </div>
114
- `).join('');
115
-
116
- // Add click handlers
117
- this.symbolOptions.querySelectorAll('.combobox-option').forEach(option => {
118
- if (!option.classList.contains('disabled')) {
119
- option.addEventListener('click', () => {
120
- const symbol = option.dataset.symbol;
121
- const item = CRYPTO_SYMBOLS.find(i => i.symbol === symbol);
122
- if (item) {
123
- this.symbol = symbol;
124
- this.symbolInput.value = `${item.symbol} - ${item.name}`;
125
- this.symbolDropdown.style.display = 'none';
126
- this.loadChart();
127
- }
128
- });
129
- }
130
- });
131
  }
132
 
133
  bindEvents() {
134
- // Timeframe buttons
 
 
 
 
 
135
  this.timeframeButtons.forEach((btn) => {
136
  btn.addEventListener('click', async () => {
137
  this.timeframeButtons.forEach((b) => b.classList.remove('active'));
138
  btn.classList.add('active');
139
- this.timeframe = btn.dataset.timeframe;
140
  await this.loadChart();
141
  });
142
  });
143
-
144
- // Load chart button
145
- if (this.loadButton) {
146
- this.loadButton.addEventListener('click', async (e) => {
147
- e.preventDefault();
148
- // Extract symbol from input
149
- const inputValue = this.symbolInput.value.trim();
150
- if (inputValue) {
151
- const match = inputValue.match(/^([A-Z0-9]+)/);
152
- if (match) {
153
- this.symbol = match[1].toUpperCase();
154
- } else {
155
- this.symbol = inputValue.toUpperCase();
156
- }
157
- }
158
- await this.loadChart();
159
- });
160
- }
161
-
162
- // Indicator buttons
163
- if (this.indicatorButtons.length > 0) {
164
- this.indicatorButtons.forEach((btn) => {
165
- btn.addEventListener('click', () => {
166
- btn.classList.toggle('active');
167
- // Don't auto-run, wait for Run Analysis button
168
- });
169
- });
170
- }
171
-
172
- // Run analysis button
173
- if (this.runAnalysisButton) {
174
- this.runAnalysisButton.addEventListener('click', async (e) => {
175
- e.preventDefault();
176
- await this.runAnalysis();
177
- });
178
  }
179
  }
180
 
181
  async loadChart() {
182
  if (!this.canvas) return;
183
-
184
- const symbol = this.symbol.trim().toUpperCase() || 'BTC';
185
- if (!symbol) {
186
- this.symbol = 'BTC';
187
- if (this.symbolInput) this.symbolInput.value = 'BTC - Bitcoin';
188
- }
189
-
190
- const container = this.canvas.closest('.chart-wrapper') || this.canvas.parentElement;
191
-
192
- // Show loading state
193
- if (container) {
194
- let loadingNode = container.querySelector('.chart-loading');
195
- if (!loadingNode) {
196
- loadingNode = document.createElement('div');
197
- loadingNode.className = 'chart-loading';
198
- container.insertBefore(loadingNode, this.canvas);
199
- }
200
- loadingNode.innerHTML = `
201
- <div class="loading-spinner"></div>
202
- <p>Loading ${symbol} chart data...</p>
203
- `;
204
- }
205
-
206
- // Update title
207
- if (this.chartTitle) {
208
- this.chartTitle.textContent = `${symbol} Price Chart (${this.timeframe})`;
209
- }
210
-
211
- try {
212
- const result = await apiClient.getPriceChart(symbol, this.timeframe);
213
-
214
- // Remove loading
215
- if (container) {
216
- const loadingNode = container.querySelector('.chart-loading');
217
- if (loadingNode) loadingNode.remove();
218
- }
219
-
220
- if (!result.ok) {
221
- const errorAnalysis = errorHelper.analyzeError(new Error(result.error), { symbol, timeframe: this.timeframe });
222
-
223
- if (container) {
224
- let errorNode = container.querySelector('.chart-error');
225
- if (!errorNode) {
226
- errorNode = document.createElement('div');
227
- errorNode.className = 'inline-message inline-error chart-error';
228
- container.appendChild(errorNode);
229
- }
230
- errorNode.innerHTML = `
231
- <strong>Error loading chart:</strong>
232
- <p>${result.error || 'Failed to load chart data'}</p>
233
- <p><small>Symbol: ${symbol} | Timeframe: ${this.timeframe}</small></p>
234
- `;
235
- }
236
- return;
237
- }
238
-
239
  if (container) {
240
- const errorNode = container.querySelector('.chart-error');
241
- if (errorNode) errorNode.remove();
242
- }
243
-
244
- // Parse chart data
245
- const chartData = result.data || {};
246
- const points = chartData.data || chartData || [];
247
-
248
- if (!points || points.length === 0) {
249
- if (container) {
250
- const errorNode = document.createElement('div');
251
- errorNode.className = 'inline-message inline-warn';
252
- errorNode.innerHTML = '<strong>No data available</strong><p>No price data found for this symbol and timeframe.</p>';
253
  container.appendChild(errorNode);
254
  }
255
- return;
256
- }
257
-
258
- // Format labels and data
259
- const labels = points.map((point) => {
260
- const ts = point.time || point.timestamp || point.date;
261
- if (!ts) return '';
262
- const date = new Date(ts);
263
- if (this.timeframe === '1d') {
264
- return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
265
- }
266
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
267
- });
268
-
269
- const prices = points.map((point) => {
270
- const price = point.price || point.close || point.value || 0;
271
- return parseFloat(price) || 0;
272
- });
273
-
274
- // Destroy existing chart
275
- if (this.chart) {
276
- this.chart.destroy();
277
- }
278
-
279
- // Calculate min/max for better scaling
280
- const minPrice = Math.min(...prices);
281
- const maxPrice = Math.max(...prices);
282
- const priceRange = maxPrice - minPrice;
283
- const firstPrice = prices[0];
284
- const lastPrice = prices[prices.length - 1];
285
- const priceChange = lastPrice - firstPrice;
286
- const priceChangePercent = ((priceChange / firstPrice) * 100).toFixed(2);
287
- const isPriceUp = priceChange >= 0;
288
-
289
- // Get indicator states
290
- const showMA20 = this.section.querySelector('[data-indicator="MA20"]')?.checked || false;
291
- const showMA50 = this.section.querySelector('[data-indicator="MA50"]')?.checked || false;
292
- const showRSI = this.section.querySelector('[data-indicator="RSI"]')?.checked || false;
293
- const showVolume = this.section.querySelector('[data-indicator="Volume"]')?.checked || false;
294
-
295
- // Prepare price data for TradingView chart
296
- const priceData = points.map((point, index) => ({
297
- time: point.time || point.timestamp || point.date || new Date().getTime() + (index * 60000),
298
- price: parseFloat(point.price || point.close || point.value || 0),
299
- volume: parseFloat(point.volume || 0)
300
- }));
301
-
302
- // Create TradingView-style chart with indicators
303
- this.chart = createAdvancedLineChart('chart-lab-canvas', priceData, {
304
- showMA20,
305
- showMA50,
306
- showRSI,
307
- showVolume
308
- });
309
-
310
- // If volume is enabled, create separate volume chart
311
- if (showVolume && priceData.some(p => p.volume > 0)) {
312
- const volumeContainer = this.section.querySelector('[data-volume-chart]');
313
- if (volumeContainer) {
314
- createVolumeChart('volume-chart-canvas', priceData);
315
- }
316
- }
317
-
318
- // Update legend with TradingView-style info
319
- if (this.chartLegend && prices.length > 0) {
320
- const currentPrice = prices[prices.length - 1];
321
- const firstPrice = prices[0];
322
- const change = currentPrice - firstPrice;
323
- const changePercent = ((change / firstPrice) * 100).toFixed(2);
324
- const isUp = change >= 0;
325
-
326
- this.chartLegend.innerHTML = `
327
- <div class="legend-item">
328
- <span class="legend-label">Price</span>
329
- <span class="legend-value">$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
330
- </div>
331
- <div class="legend-item">
332
- <span class="legend-label">24h</span>
333
- <span class="legend-value ${isUp ? 'positive' : 'negative'}">
334
- <span class="legend-arrow">${isUp ? '↑' : '↓'}</span>
335
- ${isUp ? '+' : ''}${changePercent}%
336
- </span>
337
- </div>
338
- <div class="legend-item">
339
- <span class="legend-label">High</span>
340
- <span class="legend-value">$${maxPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
341
- </div>
342
- <div class="legend-item">
343
- <span class="legend-label">Low</span>
344
- <span class="legend-value">$${minPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
345
- </div>
346
- `;
347
- }
348
- } catch (error) {
349
- console.error('Chart loading error:', error);
350
- if (container) {
351
- const errorNode = document.createElement('div');
352
- errorNode.className = 'inline-message inline-error';
353
- errorNode.innerHTML = `<strong>Error:</strong><p>${error.message || 'Failed to load chart'}</p>`;
354
- container.appendChild(errorNode);
355
  }
 
356
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  }
358
 
359
  async runAnalysis() {
360
- if (!this.analysisOutput) return;
361
-
362
- const enabledIndicators = Array.from(this.indicatorButtons)
363
- .filter((btn) => btn.classList.contains('active'))
364
- .map((btn) => btn.dataset.indicator);
365
-
366
- this.analysisOutput.innerHTML = `
367
- <div class="analysis-loading">
368
- <div class="loading-spinner"></div>
369
- <p>Running AI analysis with ${enabledIndicators.length > 0 ? enabledIndicators.join(', ') : 'default'} indicators...</p>
370
- </div>
371
- `;
372
-
373
- try {
374
- const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators);
375
-
376
- if (!result.ok) {
377
- this.analysisOutput.innerHTML = `
378
- <div class="inline-message inline-error">
379
- <strong>Analysis Error:</strong>
380
- <p>${result.error || 'Failed to run analysis'}</p>
381
- </div>
382
- `;
383
- return;
384
- }
385
-
386
- const data = result.data || {};
387
- const analysis = data.analysis || data;
388
-
389
- if (!analysis) {
390
- this.analysisOutput.innerHTML = '<div class="inline-message inline-warn">No AI insights returned.</div>';
391
- return;
392
- }
393
-
394
- const summary = analysis.summary || analysis.narrative?.summary || 'No summary available.';
395
- const signals = analysis.signals || {};
396
- const direction = analysis.change_direction || 'N/A';
397
- const changePercent = analysis.change_percent ?? '—';
398
- const high = analysis.high ?? '—';
399
- const low = analysis.low ?? '—';
400
-
401
- const bullets = Object.entries(signals)
402
- .map(([key, value]) => {
403
- const label = value?.label || value || 'n/a';
404
- const score = value?.score ?? value?.value ?? '—';
405
- return `<li><strong>${key.toUpperCase()}:</strong> ${label} ${score !== '—' ? `(${score})` : ''}</li>`;
406
- })
407
- .join('');
408
-
409
- this.analysisOutput.innerHTML = `
410
- <div class="analysis-results">
411
- <div class="analysis-header">
412
- <h5>Analysis Results</h5>
413
- <span class="analysis-badge ${direction.toLowerCase()}">${direction}</span>
414
- </div>
415
- <div class="analysis-metrics">
416
- <div class="metric-item">
417
- <span class="metric-label">Direction</span>
418
- <span class="metric-value ${direction.toLowerCase()}">${direction}</span>
419
- </div>
420
- <div class="metric-item">
421
- <span class="metric-label">Change</span>
422
- <span class="metric-value ${changePercent >= 0 ? 'positive' : 'negative'}">
423
- ${changePercent >= 0 ? '+' : ''}${changePercent}%
424
- </span>
425
- </div>
426
- <div class="metric-item">
427
- <span class="metric-label">High</span>
428
- <span class="metric-value">$${high}</span>
429
- </div>
430
- <div class="metric-item">
431
- <span class="metric-label">Low</span>
432
- <span class="metric-value">$${low}</span>
433
- </div>
434
- </div>
435
- <div class="analysis-summary">
436
- <h6>Summary</h6>
437
- <p>${summary}</p>
438
- </div>
439
- ${bullets ? `
440
- <div class="analysis-signals">
441
- <h6>Signals</h6>
442
- <ul>${bullets}</ul>
443
- </div>
444
- ` : ''}
445
- </div>
446
- `;
447
- } catch (error) {
448
- console.error('Analysis error:', error);
449
- this.analysisOutput.innerHTML = `
450
- <div class="inline-message inline-error">
451
- <strong>Error:</strong>
452
- <p>${error.message || 'Failed to run analysis'}</p>
453
- </div>
454
- `;
455
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  }
457
  }
458
 
 
1
  import apiClient from './apiClient.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  class ChartLabView {
4
  constructor(section) {
5
  this.section = section;
6
+ this.symbolSelect = section.querySelector('[data-chart-symbol]');
7
+ this.timeframeButtons = section.querySelectorAll('[data-chart-timeframe]');
8
+ this.indicatorInputs = section.querySelectorAll('[data-indicator]');
9
+ this.analyzeButton = section.querySelector('[data-run-analysis]');
10
+ this.canvas = section.querySelector('#chart-lab-canvas');
11
+ this.insightsContainer = section.querySelector('[data-ai-insights]');
 
 
 
 
 
12
  this.chart = null;
13
  this.symbol = 'BTC';
14
  this.timeframe = '7d';
 
15
  }
16
 
17
  async init() {
 
 
18
  await this.loadChart();
19
+ this.bindEvents();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
  bindEvents() {
23
+ if (this.symbolSelect) {
24
+ this.symbolSelect.addEventListener('change', async () => {
25
+ this.symbol = this.symbolSelect.value;
26
+ await this.loadChart();
27
+ });
28
+ }
29
  this.timeframeButtons.forEach((btn) => {
30
  btn.addEventListener('click', async () => {
31
  this.timeframeButtons.forEach((b) => b.classList.remove('active'));
32
  btn.classList.add('active');
33
+ this.timeframe = btn.dataset.chartTimeframe;
34
  await this.loadChart();
35
  });
36
  });
37
+ if (this.analyzeButton) {
38
+ this.analyzeButton.addEventListener('click', () => this.runAnalysis());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  }
40
  }
41
 
42
  async loadChart() {
43
  if (!this.canvas) return;
44
+ const result = await apiClient.getPriceChart(this.symbol, this.timeframe);
45
+ const container = this.canvas.parentElement;
46
+ if (!result.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  if (container) {
48
+ let errorNode = container.querySelector('.chart-error');
49
+ if (!errorNode) {
50
+ errorNode = document.createElement('div');
51
+ errorNode.className = 'inline-message inline-error chart-error';
 
 
 
 
 
 
 
 
 
52
  container.appendChild(errorNode);
53
  }
54
+ errorNode.textContent = result.error;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
+ return;
57
  }
58
+ if (container) {
59
+ const errorNode = container.querySelector('.chart-error');
60
+ if (errorNode) errorNode.remove();
61
+ }
62
+ const points = result.data || [];
63
+ const labels = points.map((point) => point.time || point.timestamp || '');
64
+ const prices = points.map((point) => point.price || point.close || point.value);
65
+ if (this.chart) {
66
+ this.chart.destroy();
67
+ }
68
+ this.chart = new Chart(this.canvas, {
69
+ type: 'line',
70
+ data: {
71
+ labels,
72
+ datasets: [
73
+ {
74
+ label: `${this.symbol} (${this.timeframe})`,
75
+ data: prices,
76
+ borderColor: '#f472b6',
77
+ backgroundColor: 'rgba(244, 114, 182, 0.2)',
78
+ fill: true,
79
+ tension: 0.4,
80
+ },
81
+ ],
82
+ },
83
+ options: {
84
+ scales: {
85
+ x: { ticks: { color: 'var(--text-muted)' } },
86
+ y: { ticks: { color: 'var(--text-muted)' } },
87
+ },
88
+ plugins: {
89
+ legend: { display: false },
90
+ },
91
+ },
92
+ });
93
  }
94
 
95
  async runAnalysis() {
96
+ if (!this.insightsContainer) return;
97
+ const enabledIndicators = Array.from(this.indicatorInputs)
98
+ .filter((input) => input.checked)
99
+ .map((input) => input.value);
100
+ this.insightsContainer.innerHTML = '<p>Running AI analysis...</p>';
101
+ const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators);
102
+ if (!result.ok) {
103
+ this.insightsContainer.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
104
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
+ const payload = result.data || {};
107
+ const insights = payload.insights || result.insights || payload;
108
+ if (!insights) {
109
+ this.insightsContainer.innerHTML = '<p>No AI insights returned.</p>';
110
+ return;
111
+ }
112
+ const summary =
113
+ insights.narrative?.summary?.summary || insights.narrative?.summary || insights.narrative?.summary_text;
114
+ const signals = insights.narrative?.signals || {};
115
+ const bullets = Object.entries(signals)
116
+ .map(([key, value]) => `<li><strong>${key}:</strong> ${(value?.label || 'n/a')} (${value?.score ?? '—'})</li>`)
117
+ .join('');
118
+ this.insightsContainer.innerHTML = `
119
+ <h4>AI Insights</h4>
120
+ <p><strong>Direction:</strong> ${insights.change_direction || 'N/A'} (${insights.change_percent ?? '—'}%)</p>
121
+ <p><strong>Range:</strong> High ${insights.high ?? '—'} / Low ${insights.low ?? '—'}</p>
122
+ <p>${summary || insights.narrative?.summary?.summary || insights.narrative?.summary || ''}</p>
123
+ <ul>${bullets || '<li>No sentiment signals provided.</li>'}</ul>
124
+ `;
125
  }
126
  }
127
 
static/js/datasetsModelsView.js CHANGED
@@ -54,9 +54,7 @@ class DatasetsModelsView {
54
  this.datasetsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
55
  return;
56
  }
57
- // Backend returns {success: true, datasets: [...], count: ...}, so access result.data.datasets
58
- const data = result.data || {};
59
- this.datasets = data.datasets || data || [];
60
  this.datasetsBody.innerHTML = this.datasets
61
  .map(
62
  (dataset) => `
@@ -83,9 +81,7 @@ class DatasetsModelsView {
83
  this.previewContent.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
84
  return;
85
  }
86
- // Backend returns {success: true, sample: [...], ...}, so access result.data.sample
87
- const data = result.data || {};
88
- const rows = data.sample || data || [];
89
  if (!rows.length) {
90
  this.previewContent.innerHTML = '<p>No sample rows available.</p>';
91
  return;
@@ -115,9 +111,7 @@ class DatasetsModelsView {
115
  this.modelsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
116
  return;
117
  }
118
- // Backend returns {success: true, models: [...], count: ...}, so access result.data.models
119
- const data = result.data || {};
120
- this.models = data.models || data || [];
121
  this.modelsBody.innerHTML = this.models
122
  .map(
123
  (model) => `
 
54
  this.datasetsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
55
  return;
56
  }
57
+ this.datasets = result.data || [];
 
 
58
  this.datasetsBody.innerHTML = this.datasets
59
  .map(
60
  (dataset) => `
 
81
  this.previewContent.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
82
  return;
83
  }
84
+ const rows = result.data || [];
 
 
85
  if (!rows.length) {
86
  this.previewContent.innerHTML = '<p>No sample rows available.</p>';
87
  return;
 
111
  this.modelsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
112
  return;
113
  }
114
+ this.models = result.data || [];
 
 
115
  this.modelsBody.innerHTML = this.models
116
  .map(
117
  (model) => `
static/js/debugConsoleView.js CHANGED
@@ -4,8 +4,8 @@ class DebugConsoleView {
4
  constructor(section, wsClient) {
5
  this.section = section;
6
  this.wsClient = wsClient;
7
- this.healthInfo = section.querySelector('[data-health-info]');
8
- this.wsInfo = section.querySelector('[data-ws-info]');
9
  this.requestLogBody = section.querySelector('[data-request-log]');
10
  this.errorLogBody = section.querySelector('[data-error-log]');
11
  this.wsLogBody = section.querySelector('[data-ws-log]');
@@ -25,31 +25,29 @@ class DebugConsoleView {
25
 
26
  async refresh() {
27
  const [health, providers] = await Promise.all([apiClient.getHealth(), apiClient.getProviders()]);
28
-
29
- // Update health info
30
- if (this.healthInfo) {
31
- if (health.ok) {
32
- const data = health.data || {};
33
- this.healthInfo.innerHTML = `
34
- <p><strong>Status:</strong> <span class="text-success">${data.status || 'OK'}</span></p>
35
- <p><strong>Uptime:</strong> ${data.uptime || 'N/A'}</p>
36
- <p><strong>Version:</strong> ${data.version || 'N/A'}</p>
37
- `;
38
- } else {
39
- this.healthInfo.innerHTML = `<div class="inline-message inline-error">${health.error || 'Unavailable'}</div>`;
40
- }
41
  }
42
-
43
- // Update WebSocket info
44
- if (this.wsInfo) {
45
- const status = this.wsClient.status || 'disconnected';
46
- const events = this.wsClient.getEvents();
47
- this.wsInfo.innerHTML = `
48
- <p><strong>Status:</strong> <span class="${status === 'connected' ? 'text-success' : 'text-danger'}">${status}</span></p>
49
- <p><strong>Events:</strong> ${events.length}</p>
50
- `;
 
 
 
 
 
 
 
 
51
  }
52
-
53
  this.renderRequestLogs();
54
  this.renderErrorLogs();
55
  this.renderWsLogs();
 
4
  constructor(section, wsClient) {
5
  this.section = section;
6
  this.wsClient = wsClient;
7
+ this.healthStatus = section.querySelector('[data-health-status]');
8
+ this.providersContainer = section.querySelector('[data-providers]');
9
  this.requestLogBody = section.querySelector('[data-request-log]');
10
  this.errorLogBody = section.querySelector('[data-error-log]');
11
  this.wsLogBody = section.querySelector('[data-ws-log]');
 
25
 
26
  async refresh() {
27
  const [health, providers] = await Promise.all([apiClient.getHealth(), apiClient.getProviders()]);
28
+ if (health.ok) {
29
+ this.healthStatus.textContent = health.data?.status || 'OK';
30
+ } else {
31
+ this.healthStatus.textContent = 'Unavailable';
 
 
 
 
 
 
 
 
 
32
  }
33
+ if (providers.ok) {
34
+ const list = providers.data || [];
35
+ this.providersContainer.innerHTML = list
36
+ .map(
37
+ (provider) => `
38
+ <div class="glass-card">
39
+ <h4>${provider.name}</h4>
40
+ <p>Status: <span class="${provider.status === 'healthy' ? 'text-success' : 'text-danger'}">${
41
+ provider.status || 'unknown'
42
+ }</span></p>
43
+ <p>Latency: ${provider.latency || '—'}ms</p>
44
+ </div>
45
+ `,
46
+ )
47
+ .join('');
48
+ } else {
49
+ this.providersContainer.innerHTML = `<div class="inline-message inline-error">${providers.error}</div>`;
50
  }
 
51
  this.renderRequestLogs();
52
  this.renderErrorLogs();
53
  this.renderWsLogs();
static/js/marketView.js CHANGED
@@ -70,9 +70,7 @@ class MarketView {
70
  </td></tr>`;
71
  return;
72
  }
73
- // Backend returns {success: true, coins: [...], count: ...}, so access result.data.coins
74
- const data = result.data || {};
75
- this.coins = data.coins || data || [];
76
  this.filtered = [...this.coins];
77
  this.renderTable();
78
  }
@@ -97,15 +95,7 @@ class MarketView {
97
  </td>
98
  <td>${coin.name || 'Unknown'}</td>
99
  <td>${formatCurrency(coin.price)}</td>
100
- <td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">
101
- <span class="table-change-icon ${coin.change_24h >= 0 ? 'positive' : 'negative'}">
102
- ${coin.change_24h >= 0 ?
103
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
104
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
105
- }
106
- </span>
107
- ${formatPercent(coin.change_24h)}
108
- </td>
109
  <td>${formatCurrency(coin.volume_24h)}</td>
110
  <td>${formatCurrency(coin.market_cap)}</td>
111
  </tr>
@@ -164,10 +154,7 @@ class MarketView {
164
  this.chartWrapper.innerHTML = `<div class="inline-message inline-error">${chart.error}</div>`;
165
  }
166
  } else {
167
- // Backend returns {success: true, data: [...], ...}, so access result.data.data
168
- const chartData = chart.data || {};
169
- const points = chartData.data || chartData || [];
170
- this.renderChart(points);
171
  }
172
  }
173
 
 
70
  </td></tr>`;
71
  return;
72
  }
73
+ this.coins = result.data || [];
 
 
74
  this.filtered = [...this.coins];
75
  this.renderTable();
76
  }
 
95
  </td>
96
  <td>${coin.name || 'Unknown'}</td>
97
  <td>${formatCurrency(coin.price)}</td>
98
+ <td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">${formatPercent(coin.change_24h)}</td>
 
 
 
 
 
 
 
 
99
  <td>${formatCurrency(coin.volume_24h)}</td>
100
  <td>${formatCurrency(coin.market_cap)}</td>
101
  </tr>
 
154
  this.chartWrapper.innerHTML = `<div class="inline-message inline-error">${chart.error}</div>`;
155
  }
156
  } else {
157
+ this.renderChart(chart.data || []);
 
 
 
158
  }
159
  }
160
 
static/js/newsView.js CHANGED
@@ -48,9 +48,7 @@ class NewsView {
48
  this.tableBody.innerHTML = `<tr><td colspan="6"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
49
  return;
50
  }
51
- // Backend returns {success: true, news: [...], count: ...}, so access result.data.news
52
- const data = result.data || {};
53
- this.dataset = data.news || data || [];
54
  this.datasetMap.clear();
55
  this.dataset.forEach((item, index) => {
56
  const rowId = item.id || `${item.title}-${index}`;
 
48
  this.tableBody.innerHTML = `<tr><td colspan="6"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
49
  return;
50
  }
51
+ this.dataset = result.data || [];
 
 
52
  this.datasetMap.clear();
53
  this.dataset.forEach((item, index) => {
54
  const rowId = item.id || `${item.title}-${index}`;
static/js/overviewView.js CHANGED
@@ -1,6 +1,5 @@
1
  import apiClient from './apiClient.js';
2
  import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js';
3
- import { initMarketOverviewChart, createSparkline } from './charts-enhanced.js';
4
 
5
  class OverviewView {
6
  constructor(section) {
@@ -8,35 +7,13 @@ class OverviewView {
8
  this.statsContainer = section.querySelector('[data-overview-stats]');
9
  this.topCoinsBody = section.querySelector('[data-top-coins-body]');
10
  this.sentimentCanvas = section.querySelector('#sentiment-chart');
11
- this.marketOverviewCanvas = section.querySelector('#market-overview-chart');
12
  this.sentimentChart = null;
13
- this.marketData = [];
14
  }
15
 
16
  async init() {
17
  this.renderStatSkeletons();
18
- this.topCoinsBody.innerHTML = createSkeletonRows(6, 8);
19
- await Promise.all([
20
- this.loadStats(),
21
- this.loadTopCoins(),
22
- this.loadSentiment(),
23
- this.loadMarketOverview(),
24
- this.loadBackendInfo()
25
- ]);
26
- }
27
-
28
- async loadMarketOverview() {
29
- try {
30
- const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
31
- const data = await response.json();
32
- this.marketData = data;
33
-
34
- if (this.marketOverviewCanvas && data.length > 0) {
35
- initMarketOverviewChart(data);
36
- }
37
- } catch (error) {
38
- console.error('Error loading market overview:', error);
39
- }
40
  }
41
 
42
  renderStatSkeletons() {
@@ -57,260 +34,60 @@ class OverviewView {
57
  });
58
  return;
59
  }
60
- // Backend returns {success: true, stats: {...}}, so access result.data.stats
61
- const data = result.data || {};
62
- const stats = data.stats || data;
63
-
64
- // Debug: Log stats to see what we're getting
65
- console.log('[OverviewView] Market Stats:', stats);
66
-
67
- // Get change data from stats if available
68
- const marketCapChange = stats.market_cap_change_24h || 0;
69
- const volumeChange = stats.volume_change_24h || 0;
70
-
71
- // Get Fear & Greed Index
72
- const fearGreedValue = stats.fear_greed_value || stats.sentiment?.fear_greed_index?.value || stats.sentiment?.fear_greed_value || 50;
73
- const fearGreedClassification = stats.sentiment?.fear_greed_index?.classification || stats.sentiment?.classification ||
74
- (fearGreedValue >= 75 ? 'Extreme Greed' :
75
- fearGreedValue >= 55 ? 'Greed' :
76
- fearGreedValue >= 45 ? 'Neutral' :
77
- fearGreedValue >= 25 ? 'Fear' : 'Extreme Fear');
78
-
79
  const cards = [
80
- {
81
- label: 'Total Market Cap',
82
- value: formatCurrency(stats.total_market_cap),
83
- change: marketCapChange,
84
- icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
85
- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
86
- <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
87
- <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
88
- </svg>`,
89
- color: '#06B6D4'
90
- },
91
- {
92
- label: '24h Volume',
93
- value: formatCurrency(stats.total_volume_24h),
94
- change: volumeChange,
95
- icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
96
- <path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
97
- <path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
98
- </svg>`,
99
- color: '#3B82F6'
100
- },
101
- {
102
- label: 'BTC Dominance',
103
- value: formatPercent(stats.btc_dominance),
104
- change: (Math.random() * 0.5 - 0.25).toFixed(2),
105
- icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
106
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
107
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
108
- </svg>`,
109
- color: '#F97316'
110
- },
111
- {
112
- label: 'Fear & Greed Index',
113
- value: fearGreedValue,
114
- change: null,
115
- classification: fearGreedClassification,
116
- icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
117
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
118
- </svg>`,
119
- color: fearGreedValue >= 75 ? '#EF4444' : fearGreedValue >= 55 ? '#F97316' : fearGreedValue >= 45 ? '#3B82F6' : fearGreedValue >= 25 ? '#8B5CF6' : '#6366F1',
120
- isFearGreed: true
121
- },
122
  ];
123
  this.statsContainer.innerHTML = cards
124
  .map(
125
- (card) => {
126
- const changeValue = card.change ? parseFloat(card.change) : 0;
127
- const isPositive = changeValue >= 0;
128
-
129
- // Special handling for Fear & Greed Index
130
- if (card.isFearGreed) {
131
- const fgColor = card.color;
132
- const fgGradient = fearGreedValue >= 75 ? 'linear-gradient(135deg, #EF4444, #DC2626)' :
133
- fearGreedValue >= 55 ? 'linear-gradient(135deg, #F97316, #EA580C)' :
134
- fearGreedValue >= 45 ? 'linear-gradient(135deg, #3B82F6, #2563EB)' :
135
- fearGreedValue >= 25 ? 'linear-gradient(135deg, #8B5CF6, #7C3AED)' :
136
- 'linear-gradient(135deg, #6366F1, #4F46E5)';
137
-
138
- return `
139
- <div class="glass-card stat-card fear-greed-card" style="--card-color: ${fgColor}">
140
- <div class="stat-header">
141
- <div class="stat-icon" style="color: ${fgColor}; background: ${fgGradient};">
142
- ${card.icon}
143
- </div>
144
- <h3>${card.label}</h3>
145
- </div>
146
- <div class="stat-value-wrapper">
147
- <div class="stat-value fear-greed-value" style="color: ${fgColor}; font-size: 2.5rem; font-weight: 800;">
148
- ${card.value}
149
- </div>
150
- <div class="fear-greed-classification" style="color: ${fgColor}; font-weight: 600; font-size: 0.875rem; margin-top: 8px;">
151
- ${card.classification}
152
- </div>
153
- </div>
154
- <div class="fear-greed-gauge" style="margin-top: 16px;">
155
- <div class="gauge-bar" style="background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%); height: 8px; border-radius: 4px; position: relative; overflow: hidden;">
156
- <div class="gauge-indicator" style="position: absolute; left: ${fearGreedValue}%; top: 50%; transform: translate(-50%, -50%); width: 16px; height: 16px; background: ${fgColor}; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 8px ${fgColor};"></div>
157
- </div>
158
- <div class="gauge-labels" style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 0.75rem; color: var(--text-muted);">
159
- <span>Extreme Fear</span>
160
- <span>Neutral</span>
161
- <span>Extreme Greed</span>
162
- </div>
163
- </div>
164
- <div class="stat-metrics">
165
- <div class="stat-metric">
166
- <span class="metric-label">Status</span>
167
- <span class="metric-value" style="color: ${fgColor};">
168
- ${card.classification}
169
- </span>
170
- </div>
171
- <div class="stat-metric">
172
- <span class="metric-label">Updated</span>
173
- <span class="metric-value">
174
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;">
175
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
176
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
177
- </svg>
178
- ${new Date().toLocaleTimeString()}
179
- </span>
180
- </div>
181
- </div>
182
- </div>
183
- `;
184
- }
185
-
186
- return `
187
- <div class="glass-card stat-card" style="--card-color: ${card.color}">
188
- <div class="stat-header">
189
- <div class="stat-icon" style="color: ${card.color}">
190
- ${card.icon}
191
- </div>
192
- <h3>${card.label}</h3>
193
- </div>
194
- <div class="stat-value-wrapper">
195
- <div class="stat-value">${card.value}</div>
196
- ${card.change !== null && card.change !== undefined ? `
197
- <div class="stat-change ${isPositive ? 'positive' : 'negative'}">
198
- <div class="change-icon-wrapper ${isPositive ? 'positive' : 'negative'}">
199
- ${isPositive ?
200
- '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
201
- '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
202
- }
203
- </div>
204
- <span class="change-value">${isPositive ? '+' : ''}${changeValue.toFixed(2)}%</span>
205
- </div>
206
- ` : ''}
207
- </div>
208
- <div class="stat-metrics">
209
- <div class="stat-metric">
210
- <span class="metric-label">24h Change</span>
211
- <span class="metric-value ${card.change !== null && card.change !== undefined ? (isPositive ? 'positive' : 'negative') : ''}">
212
- ${card.change !== null && card.change !== undefined ? `
213
- <span class="metric-icon ${isPositive ? 'positive' : 'negative'}">
214
- ${isPositive ? '↑' : '↓'}
215
- </span>
216
- ${isPositive ? '+' : ''}${changeValue.toFixed(2)}%
217
- ` : '—'}
218
- </span>
219
- </div>
220
- <div class="stat-metric">
221
- <span class="metric-label">Updated</span>
222
- <span class="metric-value">
223
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;">
224
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
225
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
226
- </svg>
227
- ${new Date().toLocaleTimeString()}
228
- </span>
229
- </div>
230
- </div>
231
  </div>
232
- `;
233
- }
234
  )
235
  .join('');
236
  }
237
 
238
  async loadTopCoins() {
239
- // Use CoinGecko API directly for better data
240
- try {
241
- const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
242
- const coins = await response.json();
243
-
244
- const rows = coins.map((coin, index) => {
245
- const sparklineId = `sparkline-${coin.id}`;
246
- const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
247
-
248
- return `
249
- <tr>
250
- <td>${index + 1}</td>
251
- <td>
252
- <div class="chip">${coin.symbol.toUpperCase()}</div>
253
- </td>
254
- <td>
255
- <div style="display: flex; align-items: center; gap: 8px;">
256
- <img src="${coin.image}" alt="${coin.name}" style="width: 24px; height: 24px; border-radius: 50%;">
257
- <span>${coin.name}</span>
258
- </div>
259
- </td>
260
- <td style="font-weight: 600;">${formatCurrency(coin.current_price)}</td>
261
- <td class="${coin.price_change_percentage_24h >= 0 ? 'text-success' : 'text-danger'}">
262
- <span class="table-change-icon ${coin.price_change_percentage_24h >= 0 ? 'positive' : 'negative'}">
263
- ${coin.price_change_percentage_24h >= 0 ?
264
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
265
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
266
- }
267
- </span>
268
- ${formatPercent(coin.price_change_percentage_24h)}
269
- </td>
270
- <td>${formatCurrency(coin.total_volume)}</td>
271
- <td>${formatCurrency(coin.market_cap)}</td>
272
- <td>
273
- <div style="width: 100px; height: 40px;">
274
- <canvas id="${sparklineId}" width="100" height="40"></canvas>
275
- </div>
276
- </td>
277
- </tr>
278
- `;
279
- });
280
-
281
- this.topCoinsBody.innerHTML = rows.join('');
282
-
283
- // Create sparkline charts after DOM update
284
- setTimeout(() => {
285
- coins.forEach(coin => {
286
- if (coin.sparkline_in_7d && coin.sparkline_in_7d.price) {
287
- const sparklineId = `sparkline-${coin.id}`;
288
- const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
289
- createSparkline(sparklineId, coin.sparkline_in_7d.price.slice(-24), changeColor);
290
- }
291
- });
292
- }, 100);
293
-
294
- } catch (error) {
295
- console.error('Error loading top coins:', error);
296
  this.topCoinsBody.innerHTML = `
297
- <tr><td colspan="8">
298
  <div class="inline-message inline-error">
299
  <strong>Failed to load coins</strong>
300
- <p>${error.message}</p>
301
  </div>
302
  </td></tr>`;
 
303
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
 
306
  async loadSentiment() {
307
  if (!this.sentimentCanvas) return;
308
- const container = this.sentimentCanvas.closest('.glass-card');
309
- if (!container) return;
310
-
311
  const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' });
312
  if (!result.ok) {
313
- container.innerHTML = this.buildSentimentFallback(result.error);
314
  return;
315
  }
316
  const payload = result.data || {};
@@ -320,142 +97,40 @@ class OverviewView {
320
  neutral: sentiment.neutral ?? 35,
321
  bearish: sentiment.bearish ?? 25,
322
  };
323
-
324
- // Calculate total for percentage
325
- const total = data.bullish + data.neutral + data.bearish;
326
- const bullishPct = total > 0 ? (data.bullish / total * 100).toFixed(1) : 0;
327
- const neutralPct = total > 0 ? (data.neutral / total * 100).toFixed(1) : 0;
328
- const bearishPct = total > 0 ? (data.bearish / total * 100).toFixed(1) : 0;
329
-
330
- // Create modern sentiment UI
331
- container.innerHTML = `
332
- <div class="sentiment-modern">
333
- <div class="sentiment-header">
334
- <h4>Global Sentiment</h4>
335
- <span class="sentiment-badge">AI Powered</span>
336
- </div>
337
- <div class="sentiment-cards">
338
- <div class="sentiment-item bullish">
339
- <div class="sentiment-item-header">
340
- <div class="sentiment-icon">
341
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
342
- <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="currentColor"/>
343
- </svg>
344
- </div>
345
- <span class="sentiment-label">Bullish</span>
346
- <span class="sentiment-percent">${bullishPct}%</span>
347
- </div>
348
- <div class="sentiment-progress">
349
- <div class="sentiment-progress-bar" style="width: ${bullishPct}%; background: linear-gradient(90deg, #22c55e, #16a34a);"></div>
350
- </div>
351
- </div>
352
- <div class="sentiment-item neutral">
353
- <div class="sentiment-item-header">
354
- <div class="sentiment-icon">
355
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
356
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
357
- <path d="M8 12h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
358
- </svg>
359
- </div>
360
- <span class="sentiment-label">Neutral</span>
361
- <span class="sentiment-percent">${neutralPct}%</span>
362
- </div>
363
- <div class="sentiment-progress">
364
- <div class="sentiment-progress-bar" style="width: ${neutralPct}%; background: linear-gradient(90deg, #38bdf8, #0ea5e9);"></div>
365
- </div>
366
- </div>
367
- <div class="sentiment-item bearish">
368
- <div class="sentiment-item-header">
369
- <div class="sentiment-icon">
370
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
371
- <path d="M12 22L8.91 15.74L2 14.73L7 9.86L5.82 2.98L12 6.23L18.18 2.98L17 9.86L22 14.73L15.09 15.74L12 22Z" fill="currentColor"/>
372
- </svg>
373
- </div>
374
- <span class="sentiment-label">Bearish</span>
375
- <span class="sentiment-percent">${bearishPct}%</span>
376
- </div>
377
- <div class="sentiment-progress">
378
- <div class="sentiment-progress-bar" style="width: ${bearishPct}%; background: linear-gradient(90deg, #ef4444, #dc2626);"></div>
379
- </div>
380
- </div>
381
- </div>
382
- <div class="sentiment-summary">
383
- <div class="sentiment-summary-item">
384
- <span class="summary-label">Overall</span>
385
- <span class="summary-value ${data.bullish > data.bearish ? 'bullish' : data.bearish > data.bullish ? 'bearish' : 'neutral'}">
386
- ${data.bullish > data.bearish ? 'Bullish' : data.bearish > data.bullish ? 'Bearish' : 'Neutral'}
387
- </span>
388
- </div>
389
- <div class="sentiment-summary-item">
390
- <span class="summary-label">Confidence</span>
391
- <span class="summary-value">${Math.max(bullishPct, neutralPct, bearishPct)}%</span>
392
- </div>
393
- </div>
394
- </div>
395
- `;
396
  }
397
 
398
  buildSentimentFallback(message) {
399
- return `
400
- <div class="sentiment-modern">
401
- <div class="sentiment-header">
402
- <h4>Global Sentiment</h4>
403
- <span class="sentiment-badge">Unavailable</span>
404
- </div>
405
- <div class="inline-message inline-info" style="margin-top: 1rem;">
406
- <strong>Sentiment insight unavailable</strong>
407
- <p>${message || 'AI sentiment endpoint did not respond in time.'}</p>
408
- </div>
409
- </div>
410
  `;
411
- }
412
-
413
- async loadBackendInfo() {
414
- const backendInfoContainer = this.section.querySelector('[data-backend-info]');
415
- if (!backendInfoContainer) return;
416
-
417
- try {
418
- // Get API health
419
- const healthResult = await apiClient.getHealth();
420
- const apiStatusEl = this.section.querySelector('[data-api-status]');
421
- if (apiStatusEl) {
422
- if (healthResult.ok) {
423
- apiStatusEl.textContent = 'Healthy';
424
- apiStatusEl.style.color = '#22c55e';
425
- } else {
426
- apiStatusEl.textContent = 'Error';
427
- apiStatusEl.style.color = '#ef4444';
428
- }
429
- }
430
-
431
- // Get providers count
432
- const providersResult = await apiClient.getProviders();
433
- const providersCountEl = this.section.querySelector('[data-providers-count]');
434
- if (providersCountEl && providersResult.ok) {
435
- const providers = providersResult.data?.providers || providersResult.data || [];
436
- const activeCount = Array.isArray(providers) ? providers.filter(p => p.status === 'active' || p.status === 'online').length : 0;
437
- const totalCount = Array.isArray(providers) ? providers.length : 0;
438
- providersCountEl.textContent = `${activeCount}/${totalCount} Active`;
439
- providersCountEl.style.color = activeCount > 0 ? '#22c55e' : '#ef4444';
440
- }
441
-
442
- // Update last update time
443
- const lastUpdateEl = this.section.querySelector('[data-last-update]');
444
- if (lastUpdateEl) {
445
- lastUpdateEl.textContent = new Date().toLocaleTimeString();
446
- lastUpdateEl.style.color = 'var(--text-secondary)';
447
- }
448
-
449
- // WebSocket status is handled by app.js
450
- const wsStatusEl = this.section.querySelector('[data-ws-status]');
451
- if (wsStatusEl) {
452
- // Will be updated by wsClient status change handler
453
- wsStatusEl.textContent = 'Checking...';
454
- wsStatusEl.style.color = '#f59e0b';
455
- }
456
- } catch (error) {
457
- console.error('Error loading backend info:', error);
458
- }
459
  }
460
  }
461
 
 
1
  import apiClient from './apiClient.js';
2
  import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js';
 
3
 
4
  class OverviewView {
5
  constructor(section) {
 
7
  this.statsContainer = section.querySelector('[data-overview-stats]');
8
  this.topCoinsBody = section.querySelector('[data-top-coins-body]');
9
  this.sentimentCanvas = section.querySelector('#sentiment-chart');
 
10
  this.sentimentChart = null;
 
11
  }
12
 
13
  async init() {
14
  this.renderStatSkeletons();
15
+ this.topCoinsBody.innerHTML = createSkeletonRows(6, 6);
16
+ await Promise.all([this.loadStats(), this.loadTopCoins(), this.loadSentiment()]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
18
 
19
  renderStatSkeletons() {
 
34
  });
35
  return;
36
  }
37
+ const stats = result.data || {};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  const cards = [
39
+ { label: 'Total Market Cap', value: formatCurrency(stats.total_market_cap) },
40
+ { label: '24h Volume', value: formatCurrency(stats.total_volume_24h) },
41
+ { label: 'BTC Dominance', value: formatPercent(stats.btc_dominance) },
42
+ { label: 'ETH Dominance', value: formatPercent(stats.eth_dominance) },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  ];
44
  this.statsContainer.innerHTML = cards
45
  .map(
46
+ (card) => `
47
+ <div class="glass-card stat-card">
48
+ <h3>${card.label}</h3>
49
+ <div class="stat-value">${card.value}</div>
50
+ <div class="stat-trend">Updated ${new Date().toLocaleTimeString()}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
+ `,
 
53
  )
54
  .join('');
55
  }
56
 
57
  async loadTopCoins() {
58
+ const result = await apiClient.getTopCoins(10);
59
+ if (!result.ok) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  this.topCoinsBody.innerHTML = `
61
+ <tr><td colspan="7">
62
  <div class="inline-message inline-error">
63
  <strong>Failed to load coins</strong>
64
+ <p>${result.error}</p>
65
  </div>
66
  </td></tr>`;
67
+ return;
68
  }
69
+ const rows = (result.data || []).map(
70
+ (coin, index) => `
71
+ <tr>
72
+ <td>${index + 1}</td>
73
+ <td>${coin.symbol || coin.ticker || '—'}</td>
74
+ <td>${coin.name || 'Unknown'}</td>
75
+ <td>${formatCurrency(coin.price)}</td>
76
+ <td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">
77
+ ${formatPercent(coin.change_24h)}
78
+ </td>
79
+ <td>${formatCurrency(coin.volume_24h)}</td>
80
+ <td>${formatCurrency(coin.market_cap)}</td>
81
+ </tr>
82
+ `);
83
+ this.topCoinsBody.innerHTML = rows.join('');
84
  }
85
 
86
  async loadSentiment() {
87
  if (!this.sentimentCanvas) return;
 
 
 
88
  const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' });
89
  if (!result.ok) {
90
+ this.sentimentCanvas.replaceWith(this.buildSentimentFallback(result.error));
91
  return;
92
  }
93
  const payload = result.data || {};
 
97
  neutral: sentiment.neutral ?? 35,
98
  bearish: sentiment.bearish ?? 25,
99
  };
100
+ if (this.sentimentChart) {
101
+ this.sentimentChart.destroy();
102
+ }
103
+ this.sentimentChart = new Chart(this.sentimentCanvas, {
104
+ type: 'doughnut',
105
+ data: {
106
+ labels: ['Bullish', 'Neutral', 'Bearish'],
107
+ datasets: [
108
+ {
109
+ data: [data.bullish, data.neutral, data.bearish],
110
+ backgroundColor: ['#22c55e', '#38bdf8', '#ef4444'],
111
+ borderWidth: 0,
112
+ },
113
+ ],
114
+ },
115
+ options: {
116
+ cutout: '65%',
117
+ plugins: {
118
+ legend: {
119
+ labels: { color: 'var(--text-primary)', usePointStyle: true },
120
+ },
121
+ },
122
+ },
123
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
  buildSentimentFallback(message) {
127
+ const wrapper = document.createElement('div');
128
+ wrapper.className = 'inline-message inline-info';
129
+ wrapper.innerHTML = `
130
+ <strong>Sentiment insight unavailable</strong>
131
+ <p>${message || 'AI sentiment endpoint did not respond in time.'}</p>
 
 
 
 
 
 
132
  `;
133
+ return wrapper;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
  }
136
 
static/js/provider-discovery.js CHANGED
@@ -19,7 +19,7 @@ class ProviderDiscoveryEngine {
19
  this.providers = [];
20
  this.categories = new Map();
21
  this.healthStatus = new Map();
22
- this.configPath = '/static/providers_config_ultimate.json'; // Fallback path (prefer /api/providers/config)
23
  this.initialized = false;
24
  }
25
 
@@ -29,12 +29,14 @@ class ProviderDiscoveryEngine {
29
  async init() {
30
  if (this.initialized) return;
31
 
32
- // Don't log initialization - only log if providers are successfully loaded
 
33
  try {
34
  // Try to load from backend API first
35
  await this.loadProvidersFromAPI();
36
  } catch (error) {
37
- // Silently fallback to JSON file - providers are optional
 
38
  await this.loadProvidersFromJSON();
39
  }
40
 
@@ -42,11 +44,7 @@ class ProviderDiscoveryEngine {
42
  this.startHealthMonitoring();
43
 
44
  this.initialized = true;
45
- // Only log if providers were successfully loaded
46
- if (this.providers.length > 0) {
47
- console.log(`[Provider Discovery] Initialized with ${this.providers.length} providers in ${this.categories.size} categories`);
48
- }
49
- // Silently skip if no providers loaded - they're optional
50
  }
51
 
52
  /**
@@ -55,35 +53,13 @@ class ProviderDiscoveryEngine {
55
  async loadProvidersFromAPI() {
56
  try {
57
  // Try the new /api/providers/config endpoint first
58
- const controller = new AbortController();
59
- const timeoutId = setTimeout(() => controller.abort(), 5000);
60
-
61
- let response = null;
62
- try {
63
- response = await fetch('/api/providers/config', {
64
- signal: controller.signal
65
- });
66
- } catch (fetchError) {
67
- // Completely suppress fetch errors - providers are optional
68
- clearTimeout(timeoutId);
69
- throw new Error('Network error');
70
- }
71
- clearTimeout(timeoutId);
72
-
73
- if (!response || !response.ok) {
74
- throw new Error(`HTTP ${response?.status || 'network error'}`);
75
- }
76
 
77
- try {
78
- const data = await response.json();
79
- this.processProviderData(data);
80
- } catch (jsonError) {
81
- // Silently handle JSON parse errors
82
- throw new Error('Invalid response');
83
- }
84
  } catch (error) {
85
- // Silently fail - will fallback to JSON
86
- throw error;
87
  }
88
  }
89
 
@@ -92,37 +68,14 @@ class ProviderDiscoveryEngine {
92
  */
93
  async loadProvidersFromJSON() {
94
  try {
95
- const controller = new AbortController();
96
- const timeoutId = setTimeout(() => controller.abort(), 5000);
97
-
98
- let response = null;
99
- try {
100
- response = await fetch(this.configPath, {
101
- signal: controller.signal
102
- });
103
- } catch (fetchError) {
104
- // Completely suppress fetch errors - providers are optional
105
- clearTimeout(timeoutId);
106
- this.useFallbackConfig();
107
- return;
108
- }
109
- clearTimeout(timeoutId);
110
-
111
- if (!response || !response.ok) {
112
- // Silently use fallback config
113
- this.useFallbackConfig();
114
- return;
115
- }
116
 
117
- try {
118
- const data = await response.json();
119
- this.processProviderData(data);
120
- } catch (jsonError) {
121
- // Silently use fallback config on parse errors
122
- this.useFallbackConfig();
123
- }
124
  } catch (error) {
125
- // Completely silent - use fallback config
 
126
  this.useFallbackConfig();
127
  }
128
  }
@@ -144,10 +97,7 @@ class ProviderDiscoveryEngine {
144
  responseTime: null
145
  }));
146
 
147
- // Only log if providers were successfully loaded
148
- if (this.providers.length > 0) {
149
- console.log(`[Provider Discovery] Loaded ${this.providers.length} providers`);
150
- }
151
  }
152
 
153
  /**
@@ -171,10 +121,7 @@ class ProviderDiscoveryEngine {
171
  providers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
172
  });
173
 
174
- // Only log if categories were created
175
- if (this.categories.size > 0) {
176
- console.log(`[Provider Discovery] Categorized into: ${Array.from(this.categories.keys()).join(', ')}`);
177
- }
178
  }
179
 
180
  /**
@@ -270,27 +217,13 @@ class ProviderDiscoveryEngine {
270
  const startTime = Date.now();
271
 
272
  try {
273
- // Call backend health check endpoint with timeout and silent error handling
274
- const controller = new AbortController();
275
- const timeoutId = setTimeout(() => controller.abort(), 5000);
276
-
277
- let response = null;
278
- try {
279
- response = await fetch(`/api/providers/${providerId}/health`, {
280
- signal: controller.signal
281
- });
282
- } catch (fetchError) {
283
- // Completely suppress fetch errors - health checks are optional
284
- clearTimeout(timeoutId);
285
- provider.status = 'unknown';
286
- provider.lastCheck = new Date();
287
- provider.responseTime = null;
288
- return { status: 'unknown' };
289
- }
290
- clearTimeout(timeoutId);
291
 
292
  const responseTime = Date.now() - startTime;
293
- const status = response && response.ok ? 'online' : 'unknown';
294
 
295
  // Update provider status
296
  provider.status = status;
@@ -305,17 +238,17 @@ class ProviderDiscoveryEngine {
305
 
306
  return { status, responseTime };
307
  } catch (error) {
308
- // Silently mark as unknown on any error
309
- provider.status = 'unknown';
310
  provider.lastCheck = new Date();
311
  provider.responseTime = null;
312
 
313
  this.healthStatus.set(providerId, {
314
- status: 'unknown',
315
- lastCheck: provider.lastCheck
 
316
  });
317
 
318
- return { status: 'unknown' };
319
  }
320
  }
321
 
@@ -333,11 +266,7 @@ class ProviderDiscoveryEngine {
333
  await this.checkProviderHealth(provider.id);
334
  }
335
 
336
- // Silently complete health checks - don't log unless there's an issue
337
- // Only log if providers are actually being monitored
338
- if (highPriorityProviders.length > 0) {
339
- // Health checks are running silently - no log needed
340
- }
341
  }, interval);
342
  }
343
 
@@ -509,10 +438,7 @@ class ProviderDiscoveryEngine {
509
  const html = providers.map(p => this.generateProviderCard(p)).join('');
510
  container.innerHTML = html;
511
 
512
- // Only log if providers were actually rendered
513
- if (providers.length > 0) {
514
- console.log(`[Provider Discovery] Rendered ${providers.length} providers`);
515
- }
516
  }
517
 
518
  /**
@@ -541,7 +467,7 @@ class ProviderDiscoveryEngine {
541
  * Use fallback minimal config
542
  */
543
  useFallbackConfig() {
544
- // Silently use fallback config - providers are optional
545
  this.providers = [
546
  {
547
  id: 'coingecko',
@@ -568,4 +494,4 @@ class ProviderDiscoveryEngine {
568
  // Export singleton instance
569
  window.providerDiscovery = new ProviderDiscoveryEngine();
570
 
571
- // Silently load engine - only log if providers are successfully initialized
 
19
  this.providers = [];
20
  this.categories = new Map();
21
  this.healthStatus = new Map();
22
+ this.configPath = '/static/providers_config_ultimate.json'; // Fallback path
23
  this.initialized = false;
24
  }
25
 
 
29
  async init() {
30
  if (this.initialized) return;
31
 
32
+ console.log('[Provider Discovery] Initializing...');
33
+
34
  try {
35
  // Try to load from backend API first
36
  await this.loadProvidersFromAPI();
37
  } catch (error) {
38
+ console.warn('[Provider Discovery] API load failed, trying JSON file:', error);
39
+ // Fallback to JSON file
40
  await this.loadProvidersFromJSON();
41
  }
42
 
 
44
  this.startHealthMonitoring();
45
 
46
  this.initialized = true;
47
+ console.log(`[Provider Discovery] Initialized with ${this.providers.length} providers in ${this.categories.size} categories`);
 
 
 
 
48
  }
49
 
50
  /**
 
53
  async loadProvidersFromAPI() {
54
  try {
55
  // Try the new /api/providers/config endpoint first
56
+ const response = await fetch('/api/providers/config');
57
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ const data = await response.json();
60
+ this.processProviderData(data);
 
 
 
 
 
61
  } catch (error) {
62
+ throw new Error(`Failed to load from API: ${error.message}`);
 
63
  }
64
  }
65
 
 
68
  */
69
  async loadProvidersFromJSON() {
70
  try {
71
+ const response = await fetch(this.configPath);
72
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
+ const data = await response.json();
75
+ this.processProviderData(data);
 
 
 
 
 
76
  } catch (error) {
77
+ console.error('[Provider Discovery] Failed to load JSON:', error);
78
+ // Use fallback minimal config
79
  this.useFallbackConfig();
80
  }
81
  }
 
97
  responseTime: null
98
  }));
99
 
100
+ console.log(`[Provider Discovery] Loaded ${this.providers.length} providers`);
 
 
 
101
  }
102
 
103
  /**
 
121
  providers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
122
  });
123
 
124
+ console.log(`[Provider Discovery] Categorized into: ${Array.from(this.categories.keys()).join(', ')}`);
 
 
 
125
  }
126
 
127
  /**
 
217
  const startTime = Date.now();
218
 
219
  try {
220
+ // Call backend health check endpoint
221
+ const response = await fetch(`/api/providers/${providerId}/health`, {
222
+ timeout: 5000
223
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
  const responseTime = Date.now() - startTime;
226
+ const status = response.ok ? 'online' : 'offline';
227
 
228
  // Update provider status
229
  provider.status = status;
 
238
 
239
  return { status, responseTime };
240
  } catch (error) {
241
+ provider.status = 'offline';
 
242
  provider.lastCheck = new Date();
243
  provider.responseTime = null;
244
 
245
  this.healthStatus.set(providerId, {
246
+ status: 'offline',
247
+ lastCheck: provider.lastCheck,
248
+ error: error.message
249
  });
250
 
251
+ return { status: 'offline', error: error.message };
252
  }
253
  }
254
 
 
266
  await this.checkProviderHealth(provider.id);
267
  }
268
 
269
+ console.log('[Provider Discovery] Health check completed');
 
 
 
 
270
  }, interval);
271
  }
272
 
 
438
  const html = providers.map(p => this.generateProviderCard(p)).join('');
439
  container.innerHTML = html;
440
 
441
+ console.log(`[Provider Discovery] Rendered ${providers.length} providers`);
 
 
 
442
  }
443
 
444
  /**
 
467
  * Use fallback minimal config
468
  */
469
  useFallbackConfig() {
470
+ console.warn('[Provider Discovery] Using minimal fallback config');
471
  this.providers = [
472
  {
473
  id: 'coingecko',
 
494
  // Export singleton instance
495
  window.providerDiscovery = new ProviderDiscoveryEngine();
496
 
497
+ console.log('[Provider Discovery] Engine loaded');
static/js/providersView.js CHANGED
@@ -33,7 +33,6 @@ class ProvidersView {
33
  this.tableBody.innerHTML = `<tr><td colspan="5"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
34
  return;
35
  }
36
- // Backend returns {providers: [...], total: ..., ...}, so access result.data.providers
37
  const data = result.data || {};
38
  this.providers = data.providers || data || [];
39
  this.applyFilters();
 
33
  this.tableBody.innerHTML = `<tr><td colspan="5"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
34
  return;
35
  }
 
36
  const data = result.data || {};
37
  this.providers = data.providers || data || [];
38
  this.applyFilters();
static/js/uiUtils.js CHANGED
@@ -1,90 +1,63 @@
1
- /**
2
- * UI Utility Functions
3
- * Works as regular script (not ES6 module)
4
- */
5
-
6
- // Create namespace object
7
- window.UIUtils = {
8
- formatCurrency: function(value) {
9
- if (value === null || value === undefined || value === '') {
10
- return '—';
11
- }
12
- const num = Number(value);
13
- if (Number.isNaN(num)) {
14
- return '—';
15
- }
16
- // Don't return '—' for 0, show $0.00 instead
17
- if (num === 0) {
18
- return '$0.00';
19
- }
20
- if (Math.abs(num) >= 1_000_000_000_000) {
21
- return `$${(num / 1_000_000_000_000).toFixed(2)}T`;
22
- }
23
- if (Math.abs(num) >= 1_000_000_000) {
24
- return `$${(num / 1_000_000_000).toFixed(2)}B`;
25
- }
26
- if (Math.abs(num) >= 1_000_000) {
27
- return `$${(num / 1_000_000).toFixed(2)}M`;
28
- }
29
- if (Math.abs(num) >= 1_000) {
30
- return `$${(num / 1_000).toFixed(2)}K`;
31
- }
32
- return `$${num.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`;
33
- },
34
-
35
- formatPercent: function(value) {
36
- if (value === null || value === undefined || Number.isNaN(Number(value))) {
37
- return '—';
38
- }
39
- const num = Number(value);
40
- return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%`;
41
- },
42
-
43
- setBadge: function(element, value) {
44
- if (!element) return;
45
- element.textContent = value;
46
- },
47
-
48
- renderMessage: function(container, { state, title, body }) {
49
- if (!container) return;
50
- container.innerHTML = `
51
- <div class="inline-message inline-${state}">
52
- <strong>${title}</strong>
53
- <p>${body}</p>
54
- </div>
55
- `;
56
- },
57
-
58
- createSkeletonRows: function(count = 3, columns = 5) {
59
- let rows = '';
60
- for (let i = 0; i < count; i += 1) {
61
- rows += '<tr class="skeleton">';
62
- for (let j = 0; j < columns; j += 1) {
63
- rows += '<td><span class="skeleton-block"></span></td>';
64
- }
65
- rows += '</tr>';
66
- }
67
- return rows;
68
- },
69
-
70
- toggleSection: function(section, active) {
71
- if (!section) return;
72
- section.classList.toggle('active', !!active);
73
- },
74
-
75
- shimmerElements: function(container) {
76
- if (!container) return;
77
- container.querySelectorAll('[data-shimmer]').forEach((el) => {
78
- el.classList.add('shimmer');
79
- });
80
  }
81
- };
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- // Also expose functions globally for backward compatibility
84
- window.formatCurrency = window.UIUtils.formatCurrency;
85
- window.formatPercent = window.UIUtils.formatPercent;
86
- window.setBadge = window.UIUtils.setBadge;
87
- window.renderMessage = window.UIUtils.renderMessage;
88
- window.createSkeletonRows = window.UIUtils.createSkeletonRows;
89
- window.toggleSection = window.UIUtils.toggleSection;
90
- window.shimmerElements = window.UIUtils.shimmerElements;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function formatCurrency(value) {
2
+ if (value === null || value === undefined || Number.isNaN(Number(value))) {
3
+ return '—';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
+ const num = Number(value);
6
+ if (Math.abs(num) >= 1_000_000_000_000) {
7
+ return `$${(num / 1_000_000_000_000).toFixed(2)}T`;
8
+ }
9
+ if (Math.abs(num) >= 1_000_000_000) {
10
+ return `$${(num / 1_000_000_000).toFixed(2)}B`;
11
+ }
12
+ if (Math.abs(num) >= 1_000_000) {
13
+ return `$${(num / 1_000_000).toFixed(2)}M`;
14
+ }
15
+ return `$${num.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
16
+ }
17
 
18
+ export function formatPercent(value) {
19
+ if (value === null || value === undefined || Number.isNaN(Number(value))) {
20
+ return '—';
21
+ }
22
+ const num = Number(value);
23
+ return `${num >= 0 ? '+' : ''}${num.toFixed(2)}%`;
24
+ }
25
+
26
+ export function setBadge(element, value) {
27
+ if (!element) return;
28
+ element.textContent = value;
29
+ }
30
+
31
+ export function renderMessage(container, { state, title, body }) {
32
+ if (!container) return;
33
+ container.innerHTML = `
34
+ <div class="inline-message inline-${state}">
35
+ <strong>${title}</strong>
36
+ <p>${body}</p>
37
+ </div>
38
+ `;
39
+ }
40
+
41
+ export function createSkeletonRows(count = 3, columns = 5) {
42
+ let rows = '';
43
+ for (let i = 0; i < count; i += 1) {
44
+ rows += '<tr class="skeleton">';
45
+ for (let j = 0; j < columns; j += 1) {
46
+ rows += '<td><span class="skeleton-block"></span></td>';
47
+ }
48
+ rows += '</tr>';
49
+ }
50
+ return rows;
51
+ }
52
+
53
+ export function toggleSection(section, active) {
54
+ if (!section) return;
55
+ section.classList.toggle('active', !!active);
56
+ }
57
+
58
+ export function shimmerElements(container) {
59
+ if (!container) return;
60
+ container.querySelectorAll('[data-shimmer]').forEach((el) => {
61
+ el.classList.add('shimmer');
62
+ });
63
+ }
static/js/wsClient.js CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * WebSocket Client for Real-time Communication
3
- * Manages WebSocket connections with automatic reconnection and exponential backoff
4
- * Supports message routing to type-specific subscribers
5
- */
6
  class WSClient {
7
  constructor() {
8
  this.socket = null;
@@ -11,80 +6,35 @@ class WSClient {
11
  this.globalSubscribers = new Set();
12
  this.typeSubscribers = new Map();
13
  this.eventLog = [];
14
- this.backoff = 1000; // Initial backoff delay in ms
15
- this.maxBackoff = 16000; // Maximum backoff delay in ms
16
  this.shouldReconnect = true;
17
- this.reconnectAttempts = 0;
18
- this.connectionStartTime = null;
19
  }
20
 
21
- /**
22
- * Automatically determine WebSocket URL based on current window location
23
- * Always uses the current origin to avoid hardcoded URLs
24
- */
25
  get url() {
26
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
27
- const host = window.location.host;
28
- return `${protocol}//${host}/ws`;
29
  }
30
 
31
- /**
32
- * Log WebSocket events for debugging and monitoring
33
- * Maintains a rolling buffer of the last 100 events
34
- * @param {Object} event - Event object to log
35
- */
36
  logEvent(event) {
37
- const entry = {
38
- ...event,
39
- time: new Date().toISOString(),
40
- attempt: this.reconnectAttempts
41
- };
42
  this.eventLog.push(entry);
43
- // Keep only last 100 events
44
- if (this.eventLog.length > 100) {
45
- this.eventLog = this.eventLog.slice(-100);
46
- }
47
- console.log('[WSClient]', entry);
48
  }
49
 
50
- /**
51
- * Subscribe to connection status changes
52
- * @param {Function} callback - Called with new status ('connecting', 'connected', 'disconnected', 'error')
53
- * @returns {Function} Unsubscribe function
54
- */
55
  onStatusChange(callback) {
56
- if (typeof callback !== 'function') {
57
- throw new Error('Callback must be a function');
58
- }
59
  this.statusSubscribers.add(callback);
60
- // Immediately call with current status
61
  callback(this.status);
62
  return () => this.statusSubscribers.delete(callback);
63
  }
64
 
65
- /**
66
- * Subscribe to all WebSocket messages
67
- * @param {Function} callback - Called with parsed message data
68
- * @returns {Function} Unsubscribe function
69
- */
70
  onMessage(callback) {
71
- if (typeof callback !== 'function') {
72
- throw new Error('Callback must be a function');
73
- }
74
  this.globalSubscribers.add(callback);
75
  return () => this.globalSubscribers.delete(callback);
76
  }
77
 
78
- /**
79
- * Subscribe to specific message types
80
- * @param {string} type - Message type to subscribe to (e.g., 'market_update', 'news_update')
81
- * @param {Function} callback - Called with messages of the specified type
82
- * @returns {Function} Unsubscribe function
83
- */
84
  subscribe(type, callback) {
85
- if (typeof callback !== 'function') {
86
- throw new Error('Callback must be a function');
87
- }
88
  if (!this.typeSubscribers.has(type)) {
89
  this.typeSubscribers.set(type, new Set());
90
  }
@@ -93,269 +43,69 @@ class WSClient {
93
  return () => set.delete(callback);
94
  }
95
 
96
- /**
97
- * Update connection status and notify all subscribers
98
- * @param {string} newStatus - New status value
99
- */
100
  updateStatus(newStatus) {
101
- if (this.status !== newStatus) {
102
- const oldStatus = this.status;
103
- this.status = newStatus;
104
- this.logEvent({
105
- type: 'status_change',
106
- from: oldStatus,
107
- to: newStatus
108
- });
109
- this.statusSubscribers.forEach(cb => {
110
- try {
111
- cb(newStatus);
112
- } catch (error) {
113
- console.error('[WSClient] Error in status subscriber:', error);
114
- }
115
- });
116
- }
117
  }
118
 
119
- /**
120
- * Establish WebSocket connection with automatic reconnection
121
- * Implements exponential backoff for reconnection attempts
122
- */
123
  connect() {
124
- // Prevent multiple simultaneous connection attempts
125
- if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) {
126
- console.log('[WSClient] Already connected or connecting');
127
  return;
128
  }
129
 
130
- this.connectionStartTime = Date.now();
131
  this.updateStatus('connecting');
132
-
133
- try {
134
- this.socket = new WebSocket(this.url);
135
- this.logEvent({
136
- type: 'connection_attempt',
137
- url: this.url,
138
- attempt: this.reconnectAttempts + 1
139
- });
140
-
141
- this.socket.onopen = () => {
142
- const connectionTime = Date.now() - this.connectionStartTime;
143
- this.backoff = 1000; // Reset backoff on successful connection
144
- this.reconnectAttempts = 0;
145
- this.updateStatus('connected');
146
- this.logEvent({
147
- type: 'connection_established',
148
- connectionTime: `${connectionTime}ms`
149
- });
150
- console.log(`[WSClient] Connected to ${this.url} in ${connectionTime}ms`);
151
- };
152
-
153
- this.socket.onmessage = (event) => {
154
- try {
155
- const data = JSON.parse(event.data);
156
- this.logEvent({
157
- type: 'message_received',
158
- messageType: data.type || 'unknown',
159
- size: event.data.length
160
- });
161
-
162
- // Notify global subscribers
163
- this.globalSubscribers.forEach(cb => {
164
- try {
165
- cb(data);
166
- } catch (error) {
167
- console.error('[WSClient] Error in global subscriber:', error);
168
- }
169
- });
170
-
171
- // Notify type-specific subscribers
172
- if (data.type && this.typeSubscribers.has(data.type)) {
173
- this.typeSubscribers.get(data.type).forEach(cb => {
174
- try {
175
- cb(data);
176
- } catch (error) {
177
- console.error(`[WSClient] Error in ${data.type} subscriber:`, error);
178
- }
179
- });
180
- }
181
- } catch (error) {
182
- console.error('[WSClient] Message parse error:', error);
183
- this.logEvent({
184
- type: 'parse_error',
185
- error: error.message,
186
- rawData: event.data.substring(0, 100)
187
- });
188
  }
189
- };
190
-
191
- this.socket.onclose = (event) => {
192
- const wasConnected = this.status === 'connected';
193
- this.updateStatus('disconnected');
194
- this.logEvent({
195
- type: 'connection_closed',
196
- code: event.code,
197
- reason: event.reason || 'No reason provided',
198
- wasClean: event.wasClean
199
- });
200
-
201
- // Attempt reconnection if enabled
202
- if (this.shouldReconnect) {
203
- this.reconnectAttempts++;
204
- const delay = this.backoff;
205
- this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
206
-
207
- console.log(`[WSClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`);
208
- this.logEvent({
209
- type: 'reconnect_scheduled',
210
- delay: `${delay}ms`,
211
- nextBackoff: `${this.backoff}ms`
212
- });
213
-
214
- setTimeout(() => this.connect(), delay);
215
- }
216
- };
217
 
218
- this.socket.onerror = (error) => {
219
- console.error('[WSClient] WebSocket error:', error);
220
- this.updateStatus('error');
221
- this.logEvent({
222
- type: 'connection_error',
223
- error: error.message || 'Unknown error',
224
- readyState: this.socket ? this.socket.readyState : 'null'
225
- });
226
- };
227
- } catch (error) {
228
- console.error('[WSClient] Failed to create WebSocket:', error);
229
- this.updateStatus('error');
230
- this.logEvent({
231
- type: 'creation_error',
232
- error: error.message
233
- });
234
-
235
- // Retry connection if enabled
236
  if (this.shouldReconnect) {
237
- this.reconnectAttempts++;
238
  const delay = this.backoff;
239
  this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
240
  setTimeout(() => this.connect(), delay);
241
  }
242
- }
 
 
 
 
 
 
 
 
243
  }
244
 
245
- /**
246
- * Gracefully disconnect WebSocket and disable automatic reconnection
247
- */
248
  disconnect() {
249
  this.shouldReconnect = false;
250
  if (this.socket) {
251
- this.logEvent({ type: 'manual_disconnect' });
252
- this.socket.close(1000, 'Client disconnect');
253
- this.socket = null;
254
- }
255
- }
256
-
257
- /**
258
- * Manually trigger reconnection (useful for testing or recovery)
259
- */
260
- reconnect() {
261
- this.disconnect();
262
- this.shouldReconnect = true;
263
- this.backoff = 1000; // Reset backoff
264
- this.reconnectAttempts = 0;
265
- this.connect();
266
- }
267
-
268
- /**
269
- * Send a message through the WebSocket connection
270
- * @param {Object} data - Data to send (will be JSON stringified)
271
- * @returns {boolean} True if sent successfully, false otherwise
272
- */
273
- send(data) {
274
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
275
- console.error('[WSClient] Cannot send message: not connected');
276
- this.logEvent({
277
- type: 'send_failed',
278
- reason: 'not_connected',
279
- readyState: this.socket ? this.socket.readyState : 'null'
280
- });
281
- return false;
282
- }
283
-
284
- try {
285
- const message = JSON.stringify(data);
286
- this.socket.send(message);
287
- this.logEvent({
288
- type: 'message_sent',
289
- messageType: data.type || 'unknown',
290
- size: message.length
291
- });
292
- return true;
293
- } catch (error) {
294
- console.error('[WSClient] Failed to send message:', error);
295
- this.logEvent({
296
- type: 'send_error',
297
- error: error.message
298
- });
299
- return false;
300
  }
301
  }
302
 
303
- /**
304
- * Get a copy of the event log
305
- * @returns {Array} Array of logged events
306
- */
307
  getEvents() {
308
  return [...this.eventLog];
309
  }
310
-
311
- /**
312
- * Get current connection statistics
313
- * @returns {Object} Connection statistics
314
- */
315
- getStats() {
316
- return {
317
- status: this.status,
318
- reconnectAttempts: this.reconnectAttempts,
319
- currentBackoff: this.backoff,
320
- maxBackoff: this.maxBackoff,
321
- shouldReconnect: this.shouldReconnect,
322
- subscriberCounts: {
323
- status: this.statusSubscribers.size,
324
- global: this.globalSubscribers.size,
325
- typed: Array.from(this.typeSubscribers.entries()).map(([type, subs]) => ({
326
- type,
327
- count: subs.size
328
- }))
329
- },
330
- eventLogSize: this.eventLog.length,
331
- url: this.url
332
- };
333
- }
334
-
335
- /**
336
- * Check if WebSocket is currently connected
337
- * @returns {boolean} True if connected
338
- */
339
- isConnected() {
340
- return this.socket && this.socket.readyState === WebSocket.OPEN;
341
- }
342
-
343
- /**
344
- * Clear all subscribers (useful for cleanup)
345
- */
346
- clearSubscribers() {
347
- this.statusSubscribers.clear();
348
- this.globalSubscribers.clear();
349
- this.typeSubscribers.clear();
350
- this.logEvent({ type: 'subscribers_cleared' });
351
- }
352
  }
353
 
354
- // Create singleton instance
355
  const wsClient = new WSClient();
356
-
357
- // Auto-connect on module load
358
- wsClient.connect();
359
-
360
- // Export singleton instance
361
- export default wsClient;
 
 
 
 
 
 
1
  class WSClient {
2
  constructor() {
3
  this.socket = null;
 
6
  this.globalSubscribers = new Set();
7
  this.typeSubscribers = new Map();
8
  this.eventLog = [];
9
+ this.backoff = 1000;
10
+ this.maxBackoff = 16000;
11
  this.shouldReconnect = true;
 
 
12
  }
13
 
 
 
 
 
14
  get url() {
15
+ const { protocol, host } = window.location;
16
+ const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
17
+ return `${wsProtocol}//${host}/ws`;
18
  }
19
 
 
 
 
 
 
20
  logEvent(event) {
21
+ const entry = { ...event, time: new Date().toISOString() };
 
 
 
 
22
  this.eventLog.push(entry);
23
+ this.eventLog = this.eventLog.slice(-100);
 
 
 
 
24
  }
25
 
 
 
 
 
 
26
  onStatusChange(callback) {
 
 
 
27
  this.statusSubscribers.add(callback);
 
28
  callback(this.status);
29
  return () => this.statusSubscribers.delete(callback);
30
  }
31
 
 
 
 
 
 
32
  onMessage(callback) {
 
 
 
33
  this.globalSubscribers.add(callback);
34
  return () => this.globalSubscribers.delete(callback);
35
  }
36
 
 
 
 
 
 
 
37
  subscribe(type, callback) {
 
 
 
38
  if (!this.typeSubscribers.has(type)) {
39
  this.typeSubscribers.set(type, new Set());
40
  }
 
43
  return () => set.delete(callback);
44
  }
45
 
 
 
 
 
46
  updateStatus(newStatus) {
47
+ this.status = newStatus;
48
+ this.statusSubscribers.forEach((cb) => cb(newStatus));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  }
50
 
 
 
 
 
51
  connect() {
52
+ if (this.socket && (this.status === 'connecting' || this.status === 'connected')) {
 
 
53
  return;
54
  }
55
 
 
56
  this.updateStatus('connecting');
57
+ this.socket = new WebSocket(this.url);
58
+ this.logEvent({ type: 'status', status: 'connecting' });
59
+
60
+ this.socket.addEventListener('open', () => {
61
+ this.backoff = 1000;
62
+ this.updateStatus('connected');
63
+ this.logEvent({ type: 'status', status: 'connected' });
64
+ });
65
+
66
+ this.socket.addEventListener('message', (event) => {
67
+ try {
68
+ const data = JSON.parse(event.data);
69
+ this.logEvent({ type: 'message', messageType: data.type || 'unknown' });
70
+ this.globalSubscribers.forEach((cb) => cb(data));
71
+ if (data.type && this.typeSubscribers.has(data.type)) {
72
+ this.typeSubscribers.get(data.type).forEach((cb) => cb(data));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
+ } catch (error) {
75
+ console.error('WS message parse error', error);
76
+ }
77
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ this.socket.addEventListener('close', () => {
80
+ this.updateStatus('disconnected');
81
+ this.logEvent({ type: 'status', status: 'disconnected' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  if (this.shouldReconnect) {
 
83
  const delay = this.backoff;
84
  this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
85
  setTimeout(() => this.connect(), delay);
86
  }
87
+ });
88
+
89
+ this.socket.addEventListener('error', (error) => {
90
+ console.error('WebSocket error', error);
91
+ this.logEvent({ type: 'error', details: error.message || 'unknown' });
92
+ if (this.socket) {
93
+ this.socket.close();
94
+ }
95
+ });
96
  }
97
 
 
 
 
98
  disconnect() {
99
  this.shouldReconnect = false;
100
  if (this.socket) {
101
+ this.socket.close();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
103
  }
104
 
 
 
 
 
105
  getEvents() {
106
  return [...this.eventLog];
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
 
110
  const wsClient = new WSClient();
111
+ export default wsClient;