Really-amin commited on
Commit
2a37caa
·
verified ·
1 Parent(s): e859121

Upload 374 files

Browse files
QUICK_START.md CHANGED
@@ -1,78 +1,221 @@
1
- # 🚀 Quick Start - 3 دقیقه تا اجرا
2
 
3
- ## روش 1: Python (ساده)
4
 
 
5
  ```bash
6
- unzip crypto-hf-integrated-final.zip
7
- cd crypto-dt-source-hf-integrated
8
- python3 -m venv venv
9
- source venv/bin/activate
10
  pip install -r requirements.txt
11
- uvicorn hf_unified_server:app --port 7860
12
  ```
13
 
14
- **سپس:** http://localhost:7860
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- ## روش 2: Docker (توصیه)
 
 
 
 
 
 
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  ```bash
19
- unzip crypto-hf-integrated-final.zip
20
- cd crypto-dt-source-hf-integrated
21
- docker build -f Dockerfile.optimized -t crypto-hub .
22
- docker run -d -p 7860:7860 --name crypto-hub crypto-hub
 
 
 
 
23
  ```
24
 
25
- **سپس:** http://localhost:7860
 
 
 
26
 
27
- ## تست
 
 
28
 
 
29
  ```bash
30
- ./test_endpoints.sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  ```
32
 
33
- ## Dashboard Tabs
 
 
34
 
35
- 1. **Overview** - نمای کلی
36
- 2. **Market** - بازار
37
- 3. **Chart Lab** - نمودارها
38
- 4. **Sentiment & AI** - احساسات (10+ models)
39
- 5. **News** - اخبار با sentiment
40
- 6. **Providers** - 95 منابع
41
- 7. **API Explorer** - تست API
42
- 8. **Diagnostics** - سلامت سیستم
43
- 9. **Datasets & Models** - 14 dataset + 10 models
44
- 10. **Settings** - تنظیمات
45
 
46
- ## Features
 
 
 
47
 
48
- - Real-time data (WebSocket)
49
- - ✅ Ensemble sentiment (10+ HF models)
50
- - 14 crypto datasets
51
- - ✅ 95 API providers
52
- - ✅ Chart analysis
53
- - News aggregation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
- ## مشکلات رایج
56
 
57
- **Port in use:**
58
  ```bash
59
- uvicorn hf_unified_server:app --port 8000
 
 
 
 
 
 
 
60
  ```
61
 
62
- **Model download:**
 
 
63
  ```bash
64
- export HF_TOKEN=your_token
 
65
  ```
66
 
67
- **Dependencies:**
68
  ```bash
69
- pip install -r requirements.txt
 
 
 
 
 
 
 
 
70
  ```
71
 
72
- ## مستندات
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- - `README_HF_INTEGRATION.md` - کامل
75
- - `DEPLOYMENT_GUIDE.md` - Production
76
- - `ADMIN_HTML_INTEGRATION.md` - Frontend
77
 
78
- **Ready!** 🚀
 
1
+ # 🚀 راهنمای سریع شروع - Quick Start Guide
2
 
3
+ ## نصب و راه‌اندازی سریع
4
 
5
+ ### 1️⃣ نصب وابستگی‌ها
6
  ```bash
 
 
 
 
7
  pip install -r requirements.txt
 
8
  ```
9
 
10
+ ### 2️⃣ Import منابع از فایل‌های JSON
11
+ ```bash
12
+ python import_resources.py
13
+ ```
14
+ این اسکریپت به‌طور خودکار همه منابع را از فایل‌های JSON موجود import می‌کند.
15
+
16
+ ### 3️⃣ راه‌اندازی سرور
17
+ ```bash
18
+ # روش 1: استفاده از اسکریپت راه‌انداز
19
+ python start_server.py
20
+
21
+ # روش 2: مستقیم
22
+ python api_server_extended.py
23
 
24
+ # روش 3: با uvicorn
25
+ uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
26
+ ```
27
+
28
+ ### 4️⃣ دسترسی به داشبورد
29
+ ```
30
+ http://localhost:8000
31
+ ```
32
 
33
+ ## 📋 تب‌های داشبورد
34
+
35
+ ### 📊 Market
36
+ - آمار کلی بازار
37
+ - لیست کریپتوکارنسی‌ها
38
+ - نمودارها و ترندینگ
39
+
40
+ ### 📡 API Monitor
41
+ - وضعیت همه ارائه‌دهندگان
42
+ - زمان پاسخ
43
+ - Health Check
44
+
45
+ ### ⚡ Advanced
46
+ - Export JSON/CSV
47
+ - Backup
48
+ - Clear Cache
49
+ - Activity Logs
50
+
51
+ ### ⚙️ Admin
52
+ - افزودن API جدید
53
+ - تنظیمات
54
+ - آمار کلی
55
+
56
+ ### 🤗 HuggingFace
57
+ - مدل‌های Sentiment Analysis
58
+ - Datasets
59
+ - جستجو در Registry
60
+
61
+ ### 🔄 Pools
62
+ - مدیریت Pool‌ها
63
+ - افزودن/حذف اعضا
64
+ - چرخش دستی
65
+
66
+ ### 📋 Logs (جدید!)
67
+ - نمایش لاگ‌ها با فیلتر
68
+ - Export به JSON/CSV
69
+ - جستجو و آمار
70
+
71
+ ### 📦 Resources (جدید!)
72
+ - مدیریت منابع API
73
+ - Import/Export
74
+ - Backup
75
+ - فیلتر بر اساس Category
76
+
77
+ ## 🔧 استفاده از API
78
+
79
+ ### دریافت لاگ‌ها
80
  ```bash
81
+ # همه لاگ‌ها
82
+ curl http://localhost:8000/api/logs
83
+
84
+ # فیلتر بر اساس Level
85
+ curl http://localhost:8000/api/logs?level=error
86
+
87
+ # جستجو
88
+ curl http://localhost:8000/api/logs?search=timeout
89
  ```
90
 
91
+ ### Export لاگ‌ها
92
+ ```bash
93
+ # Export به JSON
94
+ curl http://localhost:8000/api/logs/export/json?level=error
95
 
96
+ # Export به CSV
97
+ curl http://localhost:8000/api/logs/export/csv
98
+ ```
99
 
100
+ ### مدیریت منابع
101
  ```bash
102
+ # دریافت همه منابع
103
+ curl http://localhost:8000/api/resources
104
+
105
+ # Export منابع
106
+ curl http://localhost:8000/api/resources/export/json
107
+
108
+ # Backup
109
+ curl -X POST http://localhost:8000/api/resources/backup
110
+
111
+ # Import
112
+ curl -X POST "http://localhost:8000/api/resources/import/json?file_path=api-resources/crypto_resources_unified_2025-11-11.json&merge=true"
113
+ ```
114
+
115
+ ## 📝 مثال‌های استفاده
116
+
117
+ ### افزودن Provider جدید
118
+ ```python
119
+ from resource_manager import ResourceManager
120
+
121
+ manager = ResourceManager()
122
+
123
+ provider = {
124
+ "id": "my_new_api",
125
+ "name": "My New API",
126
+ "category": "market_data",
127
+ "base_url": "https://api.example.com",
128
+ "requires_auth": False,
129
+ "priority": 5,
130
+ "weight": 50,
131
+ "free": True
132
+ }
133
+
134
+ manager.add_provider(provider)
135
+ manager.save_resources()
136
  ```
137
 
138
+ ### ثبت لاگ
139
+ ```python
140
+ from log_manager import log_info, log_error, LogCategory
141
 
142
+ # لاگ Info
143
+ log_info(LogCategory.PROVIDER, "Provider health check completed",
144
+ provider_id="coingecko", response_time=234.5)
 
 
 
 
 
 
 
145
 
146
+ # لاگ Error
147
+ log_error(LogCategory.PROVIDER, "Provider failed",
148
+ provider_id="etherscan", error="Timeout")
149
+ ```
150
 
151
+ ### استفاده از Provider Manager
152
+ ```python
153
+ from provider_manager import ProviderManager
154
+ import asyncio
155
+
156
+ async def main():
157
+ manager = ProviderManager()
158
+
159
+ # Health Check
160
+ await manager.health_check_all()
161
+
162
+ # دریافت Provider از Pool
163
+ provider = manager.get_next_from_pool("primary_market_data_pool")
164
+ if provider:
165
+ print(f"Selected: {provider.name}")
166
+
167
+ await manager.close_session()
168
+
169
+ asyncio.run(main())
170
+ ```
171
 
172
+ ## 🐳 استفاده با Docker
173
 
 
174
  ```bash
175
+ # Build
176
+ docker build -t crypto-monitor .
177
+
178
+ # Run
179
+ docker run -p 8000:8000 crypto-monitor
180
+
181
+ # یا با docker-compose
182
+ docker-compose up -d
183
  ```
184
 
185
+ ## 🔍 عیب‌یابی
186
+
187
+ ### مشکل: Port در حال استفاده است
188
  ```bash
189
+ # تغییر پورت
190
+ uvicorn api_server_extended:app --port 8001
191
  ```
192
 
193
+ ### مشکل: فایل‌های JSON یافت نشد
194
  ```bash
195
+ # بررسی وجود فایل‌ها
196
+ ls -la api-resources/
197
+ ls -la providers_config*.json
198
+ ```
199
+
200
+ ### مشکل: Import منابع ناموفق
201
+ ```bash
202
+ # بررسی ساختار JSON
203
+ python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
204
  ```
205
 
206
+ ## 📚 مستندات بیشتر
207
+
208
+ - [README.md](README.md) - مستندات کامل انگلیسی
209
+ - [README_FA.md](README_FA.md) - مستندات کامل فارسی
210
+ - [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
211
+
212
+ ## 🆘 پشتیبانی
213
+
214
+ در صورت بروز مشکل:
215
+ 1. لاگ‌ها را بررسی کنید: `logs/app.log`
216
+ 2. از تب Logs در داشبورد استفاده کنید
217
+ 3. آمار سیستم را بررسی کنید: `/api/status`
218
 
219
+ ---
 
 
220
 
221
+ **موفق باشید! 🚀**
TEST_ENDPOINTS.sh CHANGED
@@ -1,161 +1,88 @@
1
- #!/bin/bash
2
- # API Endpoints Test Script
3
- # Run this after starting the backend to verify all endpoints work
4
-
5
- BASE_URL="${BASE_URL:-http://localhost:7860}"
6
- GREEN='\033[0;32m'
7
- RED='\033[0;31m'
8
- YELLOW='\033[1;33m'
9
- NC='\033[0m' # No Color
10
-
11
- echo "======================================"
12
- echo "🧪 Testing Crypto HF API Endpoints"
13
- echo "======================================"
14
- echo "Base URL: $BASE_URL"
15
- echo ""
16
-
17
- # Function to test endpoint
18
- test_endpoint() {
19
- local method=$1
20
- local endpoint=$2
21
- local data=$3
22
- local name=$4
23
-
24
- echo -n "Testing $name... "
25
-
26
- if [ "$method" = "GET" ]; then
27
- response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$endpoint")
28
- else
29
- response=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" "$BASE_URL$endpoint" \
30
- -H "Content-Type: application/json" \
31
- -d "$data")
32
- fi
33
-
34
- if [ "$response" = "200" ]; then
35
- echo -e "${GREEN}✅ OK${NC} (HTTP $response)"
36
- else
37
- echo -e "${RED}❌ FAILED${NC} (HTTP $response)"
38
- return 1
39
- fi
40
- }
41
-
42
- # Test health
43
- test_endpoint "GET" "/api/health" "" "Health Check"
44
-
45
- # Test market endpoints
46
- echo ""
47
- echo "📊 Market Endpoints:"
48
- test_endpoint "GET" "/api/coins/top?limit=5" "" "Top Coins"
49
- test_endpoint "GET" "/api/coins/BTC" "" "Bitcoin Details"
50
- test_endpoint "GET" "/api/market/stats" "" "Market Stats"
51
-
52
- # Test chart endpoints
53
- echo ""
54
- echo "📈 Chart Endpoints:"
55
- test_endpoint "GET" "/api/charts/price/BTC?timeframe=7d" "" "BTC Price Chart"
56
-
57
- # POST endpoint for chart analyze
58
- echo -n "Testing Chart Analysis... "
59
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/charts/analyze" \
60
- -H "Content-Type: application/json" \
61
- -d '{"symbol":"BTC","timeframe":"7d","indicators":[]}')
62
- http_code=$(echo "$response" | tail -n1)
63
- if [ "$http_code" = "200" ]; then
64
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
65
- else
66
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
67
- fi
68
-
69
- # Test news endpoints
70
- echo ""
71
- echo "📰 News Endpoints:"
72
- test_endpoint "GET" "/api/news/latest?limit=5" "" "Latest News"
73
-
74
- # POST endpoint for news summarize
75
- echo -n "Testing News Summarize... "
76
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/news/summarize" \
77
- -H "Content-Type: application/json" \
78
- -d '{"title":"Bitcoin breaks new record","description":"BTC hits $50k"}')
79
- http_code=$(echo "$response" | tail -n1)
80
- if [ "$http_code" = "200" ]; then
81
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
82
- else
83
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
84
- fi
85
-
86
- # Test AI endpoints
87
- echo ""
88
- echo "🤖 AI Endpoints:"
89
-
90
- # POST endpoint for sentiment
91
- echo -n "Testing Sentiment Analysis... "
92
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/sentiment/analyze" \
93
- -H "Content-Type: application/json" \
94
- -d '{"text":"Bitcoin is breaking new all-time highs!"}')
95
- http_code=$(echo "$response" | tail -n1)
96
- body=$(echo "$response" | head -n-1)
97
- if [ "$http_code" = "200" ]; then
98
- sentiment=$(echo "$body" | grep -o '"sentiment":"[^"]*"' | cut -d'"' -f4)
99
- confidence=$(echo "$body" | grep -o '"confidence":[0-9.]*' | cut -d':' -f2)
100
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code) - Sentiment: ${YELLOW}$sentiment${NC} (${confidence})"
101
- else
102
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
103
- fi
104
-
105
- # POST endpoint for query
106
- echo -n "Testing Query... "
107
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/query" \
108
- -H "Content-Type: application/json" \
109
- -d '{"query":"What is the price of Bitcoin?"}')
110
- http_code=$(echo "$response" | tail -n1)
111
- if [ "$http_code" = "200" ]; then
112
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
113
- else
114
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
115
- fi
116
-
117
- # Test provider endpoints
118
- echo ""
119
- echo "🔌 Provider Endpoints:"
120
- test_endpoint "GET" "/api/providers" "" "Providers List"
121
-
122
- # Test datasets endpoints
123
- echo ""
124
- echo "📚 Datasets & Models Endpoints:"
125
- test_endpoint "GET" "/api/datasets/list" "" "Datasets List"
126
- test_endpoint "GET" "/api/models/list" "" "Models List"
127
-
128
- # POST endpoint for model test
129
- echo -n "Testing Model Test... "
130
- response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/models/test" \
131
- -H "Content-Type: application/json" \
132
- -d '{"model":"crypto_sent_0","text":"Ethereum price surging!"}')
133
- http_code=$(echo "$response" | tail -n1)
134
- if [ "$http_code" = "200" ]; then
135
- echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
136
- else
137
- echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
138
- fi
139
-
140
- # Summary
141
- echo ""
142
- echo "======================================"
143
- echo "📊 Test Summary"
144
- echo "======================================"
145
- echo ""
146
- echo "✅ All critical endpoints tested"
147
- echo ""
148
- echo "🌐 Dashboard URLs:"
149
- echo " - Main: $BASE_URL/"
150
- echo " - Admin: $BASE_URL/admin.html"
151
- echo " - API Docs: $BASE_URL/docs"
152
- echo ""
153
- echo "🔌 WebSocket:"
154
- echo " - ws://$(echo $BASE_URL | sed 's|http://||')/ws"
155
- echo ""
156
- echo "💡 Next steps:"
157
- echo " 1. Open $BASE_URL/ in your browser"
158
- echo " 2. Check all dashboard tabs"
159
- echo " 3. Verify WebSocket connection (status indicator)"
160
- echo ""
161
- echo "======================================"
 
1
+ #!/bin/bash
2
+ # Script to test all HuggingFace Space endpoints
3
+
4
+ BASE_URL="https://really-amin-datasourceforcryptocurrency.hf.space"
5
+
6
+ echo "=================================="
7
+ echo "🧪 Testing HuggingFace Space API"
8
+ echo "=================================="
9
+ echo ""
10
+
11
+ # Color codes
12
+ GREEN='\033[0;32m'
13
+ RED='\033[0;31m'
14
+ YELLOW='\033[1;33m'
15
+ NC='\033[0m' # No Color
16
+
17
+ test_endpoint() {
18
+ local name=$1
19
+ local endpoint=$2
20
+
21
+ echo -n "Testing $name ... "
22
+ response=$(curl -s -w "\n%{http_code}" "$BASE_URL$endpoint" 2>&1)
23
+ http_code=$(echo "$response" | tail -n1)
24
+ body=$(echo "$response" | head -n-1)
25
+
26
+ if [ "$http_code" = "200" ]; then
27
+ echo -e "${GREEN} OK${NC} (HTTP $http_code)"
28
+ return 0
29
+ else
30
+ echo -e "${RED}✗ FAILED${NC} (HTTP $http_code)"
31
+ echo " Response: $body"
32
+ return 1
33
+ fi
34
+ }
35
+
36
+ # Core Endpoints
37
+ echo "📊 Core Endpoints"
38
+ echo "==================="
39
+ test_endpoint "Health" "/health"
40
+ test_endpoint "Info" "/info"
41
+ test_endpoint "Providers" "/api/providers"
42
+ echo ""
43
+
44
+ # Data Endpoints
45
+ echo "💰 Market Data Endpoints"
46
+ echo "========================="
47
+ test_endpoint "OHLCV (BTC)" "/api/ohlcv?symbol=BTCUSDT&interval=1h&limit=10"
48
+ test_endpoint "Top Prices" "/api/crypto/prices/top?limit=5"
49
+ test_endpoint "BTC Price" "/api/crypto/price/BTC"
50
+ test_endpoint "Market Overview" "/api/crypto/market-overview"
51
+ test_endpoint "Multiple Prices" "/api/market/prices?symbols=BTC,ETH,SOL"
52
+ test_endpoint "Market Data Prices" "/api/market-data/prices?symbols=BTC,ETH"
53
+ echo ""
54
+
55
+ # Analysis Endpoints
56
+ echo "📈 Analysis Endpoints"
57
+ echo "====================="
58
+ test_endpoint "Trading Signals" "/api/analysis/signals?symbol=BTCUSDT"
59
+ test_endpoint "SMC Analysis" "/api/analysis/smc?symbol=BTCUSDT"
60
+ test_endpoint "Scoring Snapshot" "/api/scoring/snapshot?symbol=BTCUSDT"
61
+ test_endpoint "All Signals" "/api/signals"
62
+ test_endpoint "Sentiment" "/api/sentiment"
63
+ echo ""
64
+
65
+ # System Endpoints
66
+ echo "⚙️ System Endpoints"
67
+ echo "===================="
68
+ test_endpoint "System Status" "/api/system/status"
69
+ test_endpoint "System Config" "/api/system/config"
70
+ test_endpoint "Categories" "/api/categories"
71
+ test_endpoint "Rate Limits" "/api/rate-limits"
72
+ test_endpoint "Logs" "/api/logs?limit=10"
73
+ test_endpoint "Alerts" "/api/alerts"
74
+ echo ""
75
+
76
+ # HuggingFace Endpoints
77
+ echo "🤗 HuggingFace Endpoints"
78
+ echo "========================="
79
+ test_endpoint "HF Health" "/api/hf/health"
80
+ test_endpoint "HF Registry" "/api/hf/registry?kind=models"
81
+ echo ""
82
+
83
+ echo "=================================="
84
+ echo "✅ Testing Complete!"
85
+ echo "=================================="
86
+ echo ""
87
+ echo "📖 Full documentation: ${BASE_URL}/docs"
88
+ echo "📋 API Guide: See HUGGINGFACE_API_GUIDE.md"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
admin.html CHANGED
@@ -1,506 +1,1017 @@
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>Crypto Intelligence Hub - HF Space</title>
7
- <link rel="stylesheet" href="static/css/design-tokens.css" />
8
- <link rel="stylesheet" href="static/css/design-system.css" />
9
- <link rel="stylesheet" href="static/css/dashboard.css" />
10
- <link rel="stylesheet" href="static/css/pro-dashboard.css" />
11
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  </head>
13
- <body data-theme="dark">
14
- <div class="app-shell">
15
- <!-- Sidebar Navigation -->
16
- <aside class="sidebar">
17
- <div class="brand">
18
- <strong>Crypto Intelligence Hub</strong>
19
- <span class="env-pill">
20
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
21
- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
22
- <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
23
- <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
24
- </svg>
25
- HF Space
26
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  </div>
28
- <nav class="nav">
29
- <button class="nav-button active" data-nav="page-overview">
30
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/></svg>
31
- Overview
32
- </button>
33
- <button class="nav-button" data-nav="page-market">
34
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 17l6-6 4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
35
- Market
36
- </button>
37
- <button class="nav-button" data-nav="page-chart">
38
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/><path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/></svg>
39
- Chart Lab
40
- </button>
41
- <button class="nav-button" data-nav="page-ai">
42
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" fill="currentColor"/><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
43
- AI Advisor
44
- </button>
45
- <button class="nav-button" data-nav="page-news">
46
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2"/><path d="M7 10h6m-6 4h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
47
- News
48
- </button>
49
- <button class="nav-button" data-nav="page-providers">
50
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
51
- Providers
52
- </button>
53
- <button class="nav-button" data-nav="page-datasets">
54
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
55
- Datasets & Models
56
- </button>
57
- <button class="nav-button" data-nav="page-api">
58
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="currentColor"/></svg>
59
- API Explorer
60
- </button>
61
- <button class="nav-button" data-nav="page-debug">
62
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
63
- Diagnostics
64
- </button>
65
- <button class="nav-button" data-nav="page-settings">
66
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/><path d="M12 1v6m0 6v6M5 5l4 4m6 6l4 4M1 12h6m6 0h6M5 19l4-4m6-6l4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
67
- Settings
68
- </button>
69
- </nav>
70
- <div class="sidebar-footer">
71
- <small>
72
- Crypto Intelligence Hub<br />
73
- <strong>10+ HF Models</strong> • <strong>14 Datasets</strong><br />
74
- Real-time data • Ensemble sentiment
75
- </small>
76
  </div>
77
- </aside>
78
-
79
- <!-- Main Content Area -->
80
- <main class="main-area">
81
- <!-- Top Bar with Status -->
82
- <header class="topbar">
83
- <div>
84
- <h1>Crypto Intelligence Dashboard</h1>
85
- <p class="text-muted">Live market data, AI-powered sentiment analysis, and comprehensive crypto intelligence</p>
86
- </div>
87
- <div class="status-group">
88
- <div class="status-pill" data-api-health data-state="warn">
89
- <span class="status-dot"></span>
90
- <span>checking</span>
91
- </div>
92
- <div class="status-pill" data-ws-status data-state="warn">
93
- <span class="status-dot"></span>
94
- <span>connecting</span>
95
- </div>
 
 
 
 
96
  </div>
97
- </header>
98
-
99
- <div class="page-container">
100
- <!-- ========== OVERVIEW PAGE ========== -->
101
- <section id="page-overview" class="page active">
102
- <div class="section-header">
103
- <h2 class="section-title">Global Overview</h2>
104
- <span class="chip">Powered by /api/market/stats</span>
105
- </div>
106
-
107
- <!-- Market Stats Cards -->
108
- <div class="stats-grid" data-overview-stats>
109
- <div class="glass-card stat-card">
110
- <div class="stat-label">Total Market Cap</div>
111
- <div class="stat-value">Loading...</div>
112
- </div>
113
- <div class="glass-card stat-card">
114
- <div class="stat-label">24h Volume</div>
115
- <div class="stat-value">Loading...</div>
116
- </div>
117
- <div class="glass-card stat-card">
118
- <div class="stat-label">BTC Dominance</div>
119
- <div class="stat-value">Loading...</div>
120
- </div>
121
- <div class="glass-card stat-card">
122
- <div class="stat-label">Market Sentiment</div>
123
- <div class="stat-value">Loading...</div>
124
- </div>
125
- </div>
126
-
127
- <div class="grid-two">
128
- <!-- Top Coins Table -->
129
- <div class="glass-card">
130
- <div class="section-header">
131
- <h3>Top Coins</h3>
132
- <span class="text-muted">By market cap</span>
133
- </div>
134
- <div class="table-wrapper">
135
- <table>
136
- <thead>
137
- <tr>
138
- <th>#</th>
139
- <th>Symbol</th>
140
- <th>Name</th>
141
- <th>Price</th>
142
- <th>24h %</th>
143
- <th>Volume</th>
144
- <th>Market Cap</th>
145
- </tr>
146
- </thead>
147
- <tbody data-top-coins-body>
148
- <tr><td colspan="7" style="text-align:center;padding:2rem;">Loading top coins...</td></tr>
149
- </tbody>
150
- </table>
151
- </div>
152
- </div>
153
-
154
- <!-- Sentiment Chart -->
155
- <div class="glass-card">
156
- <div class="section-header">
157
- <h3>Global Sentiment</h3>
158
- <span class="text-muted">Ensemble HF models</span>
159
- </div>
160
- <canvas id="sentiment-chart" height="220"></canvas>
161
- <div style="margin-top:1rem;font-size:0.875rem;color:var(--text-secondary);">
162
- <strong>Models used:</strong> CryptoBERT, FinBERT, Twitter Sentiment<br>
163
- <strong>Method:</strong> Majority voting with confidence scoring
164
- </div>
165
- </div>
166
- </div>
167
- </section>
168
-
169
- <!-- ========== MARKET PAGE ========== -->
170
- <section id="page-market" class="page">
171
- <div class="section-header">
172
- <h2 class="section-title">Market Intelligence</h2>
173
- <div class="controls-bar">
174
- <div class="input-chip">
175
- <svg viewBox="0 0 24 24" width="16" height="16"><path d="M21 20l-5.6-5.6A6.5 6.5 0 1 0 15.4 16L21 21zM5 10.5a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0z" fill="currentColor"/></svg>
176
- <input type="text" placeholder="Search symbol" data-market-search />
177
- </div>
178
- <button class="ghost" data-refresh-market>Refresh</button>
179
- </div>
180
- </div>
 
 
 
 
 
 
 
181
 
182
- <div class="glass-card">
183
- <div class="table-wrapper">
184
- <table>
185
- <thead>
186
- <tr>
187
- <th>#</th>
188
- <th>Symbol</th>
189
- <th>Name</th>
190
- <th>Price</th>
191
- <th>24h %</th>
192
- <th>Volume</th>
193
- <th>Market Cap</th>
194
- <th>Actions</th>
195
- </tr>
196
- </thead>
197
- <tbody data-market-body>
198
- <tr><td colspan="8" style="text-align:center;padding:2rem;">Loading market data...</td></tr>
199
- </tbody>
200
- </table>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  </div>
202
- </div>
203
-
204
- <!-- Coin Detail Drawer -->
205
- <div class="drawer" data-market-drawer style="display:none;">
206
- <button class="ghost" data-close-drawer>Close</button>
207
- <h3 data-drawer-symbol>—</h3>
208
- <div data-drawer-stats></div>
209
- <div class="glass-card" data-chart-wrapper>
210
- <canvas id="market-detail-chart" height="180"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </div>
212
- <div class="glass-card">
213
- <h4>AI Sentiment Analysis</h4>
214
- <div data-drawer-sentiment></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  </div>
216
- </div>
217
- </section>
218
-
219
- <!-- ========== CHART LAB PAGE ========== -->
220
- <section id="page-chart" class="page">
221
- <div class="section-header">
222
- <h2 class="section-title">Chart Lab</h2>
223
- <div class="controls-bar">
224
- <select data-chart-symbol>
225
- <option value="BTC">Bitcoin (BTC)</option>
226
- <option value="ETH">Ethereum (ETH)</option>
227
- <option value="SOL">Solana (SOL)</option>
228
- <option value="BNB">BNB</option>
229
- <option value="XRP">Ripple (XRP)</option>
230
- <option value="ADA">Cardano (ADA)</option>
231
- </select>
232
- <div class="input-chip">
233
- <button class="ghost active" data-chart-timeframe="7d">7D</button>
234
- <button class="ghost" data-chart-timeframe="30d">30D</button>
235
- <button class="ghost" data-chart-timeframe="90d">90D</button>
236
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  </div>
238
- </div>
239
-
240
- <div class="glass-card">
241
- <canvas id="chart-lab-canvas" height="300"></canvas>
242
- </div>
243
-
244
- <div class="glass-card">
245
- <h4>Technical Analysis</h4>
246
- <div class="controls-bar">
247
- <label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
248
- <label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
249
- <label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
250
- <label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
251
  </div>
252
- <button class="primary" data-run-analysis>🤖 Analyze with AI</button>
253
- <div data-ai-insights class="ai-insights" style="margin-top:1rem;"></div>
254
- </div>
255
- </section>
256
-
257
- <!-- ========== AI ADVISOR PAGE ========== -->
258
- <section id="page-ai" class="page">
259
- <div class="section-header">
260
- <h2 class="section-title">AI-Powered Sentiment & Advisory</h2>
261
- <span class="chip">Ensemble: CryptoBERT + FinBERT + Social</span>
262
- </div>
263
-
264
- <div class="glass-card">
265
- <h4>Sentiment Analysis</h4>
266
- <form data-sentiment-form>
267
- <label>Text to Analyze
268
- <textarea name="text" rows="4" placeholder="Enter crypto-related text, news headline, or social media post for sentiment analysis..."></textarea>
269
- </label>
270
- <button class="primary" type="submit">🧠 Analyze Sentiment</button>
271
- </form>
272
- <div data-sentiment-result style="margin-top:1rem;"></div>
273
- </div>
274
-
275
- <div class="glass-card" style="margin-top:1.5rem;">
276
- <h4>AI Query Interface</h4>
277
- <form data-query-form>
278
- <label>Ask a Question
279
- <textarea name="query" rows="3" placeholder="e.g., What is the current Bitcoin price? or Analyze Ethereum trend"></textarea>
280
- </label>
281
- <button class="primary" type="submit">🔍 Submit Query</button>
282
- </form>
283
- <div data-query-result style="margin-top:1rem;"></div>
284
- </div>
285
-
286
- <div class="inline-message inline-info">
287
- ⚠️ AI-generated outputs are experimental and should not be considered financial advice.
288
- </div>
289
- </section>
290
-
291
- <!-- ========== NEWS PAGE ========== -->
292
- <section id="page-news" class="page">
293
- <div class="section-header">
294
- <h2 class="section-title">News & Headlines</h2>
295
- <span class="chip">With AI sentiment analysis</span>
296
- </div>
297
-
298
- <div class="controls-bar">
299
- <input type="text" placeholder="Search headlines..." data-news-search />
300
- <input type="text" placeholder="Filter by symbol (e.g., BTC)" data-news-symbol />
301
- <button class="ghost" data-refresh-news>Refresh</button>
302
- </div>
303
-
304
- <div class="glass-card">
305
- <div class="table-wrapper">
306
- <table>
307
- <thead>
308
- <tr>
309
- <th>Title</th>
310
- <th>Source</th>
311
- <th>Symbols</th>
312
- <th>Sentiment</th>
313
- <th>Time</th>
314
- <th>Actions</th>
315
- </tr>
316
- </thead>
317
- <tbody data-news-body>
318
- <tr><td colspan="6" style="text-align:center;padding:2rem;">Loading news...</td></tr>
319
- </tbody>
320
- </table>
321
  </div>
322
- </div>
323
- </section>
324
-
325
- <!-- ========== PROVIDERS PAGE ========== -->
326
- <section id="page-providers" class="page">
327
- <div class="section-header">
328
- <h2 class="section-title">API Providers</h2>
329
- <span class="chip">95+ data sources</span>
330
- </div>
331
-
332
- <div class="glass-card">
333
- <div class="table-wrapper">
334
- <table>
335
- <thead>
336
- <tr>
337
- <th>Provider</th>
338
- <th>Category</th>
339
- <th>Type</th>
340
- <th>Status</th>
341
- <th>Response Time</th>
342
- </tr>
343
- </thead>
344
- <tbody data-providers-body>
345
- <tr><td colspan="5" style="text-align:center;padding:2rem;">Loading providers...</td></tr>
346
- </tbody>
347
- </table>
348
  </div>
349
  </div>
350
- </section>
351
-
352
- <!-- ========== DATASETS & MODELS PAGE ========== -->
353
- <section id="page-datasets" class="page">
354
- <div class="section-header">
355
- <h2 class="section-title">HuggingFace Datasets & Models</h2>
356
- </div>
357
-
358
- <div class="grid-two">
359
- <!-- Datasets -->
360
- <div class="glass-card">
361
- <h4>📊 Crypto Datasets (14+)</h4>
362
- <div class="table-wrapper">
363
- <table>
364
- <thead>
365
- <tr>
366
- <th>Dataset</th>
367
- <th>Category</th>
368
- <th>Actions</th>
369
- </tr>
370
- </thead>
371
- <tbody data-datasets-body>
372
- <tr><td colspan="3" style="text-align:center;padding:1rem;">Loading...</td></tr>
373
- </tbody>
374
- </table>
375
- </div>
376
- </div>
377
-
378
- <!-- Models -->
379
- <div class="glass-card">
380
- <h4>🤖 AI Models (10+)</h4>
381
- <div class="table-wrapper">
382
- <table>
383
- <thead>
384
- <tr>
385
- <th>Model</th>
386
- <th>Task</th>
387
- <th>Status</th>
388
- </tr>
389
- </thead>
390
- <tbody data-models-body>
391
- <tr><td colspan="3" style="text-align:center;padding:1rem;">Loading...</td></tr>
392
- </tbody>
393
- </table>
394
  </div>
 
395
  </div>
396
- </div>
397
-
398
- <!-- Model Test Form -->
399
- <div class="glass-card" style="margin-top:1.5rem;">
400
- <h4>🧪 Test a Model</h4>
401
- <form data-model-test-form>
402
- <div class="grid-two">
403
- <label>Model
404
- <select name="model" data-model-select>
405
- <option value="">Select a model...</option>
406
- </select>
407
- </label>
408
- <label>Input Text
409
- <textarea name="input" rows="3" placeholder="Enter text to test the model..."></textarea>
410
- </label>
 
 
 
 
 
 
 
 
411
  </div>
412
- <button class="primary" type="submit">Run Test</button>
413
- </form>
414
- <div data-model-test-output style="margin-top:1rem;"></div>
415
- </div>
416
- </section>
417
-
418
- <!-- ========== API EXPLORER PAGE ========== -->
419
- <section id="page-api" class="page">
420
- <div class="section-header">
421
- <h2 class="section-title">API Explorer</h2>
422
- <span class="chip">15+ endpoints</span>
423
- </div>
424
-
425
- <div class="glass-card">
426
- <h4>Available Endpoints</h4>
427
- <div data-api-endpoints style="display:grid;gap:0.5rem;margin-top:1rem;">
428
- <!-- Will be populated by JS -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
429
  </div>
430
- </div>
431
- </section>
432
-
433
- <!-- ========== DIAGNOSTICS PAGE ========== -->
434
- <section id="page-debug" class="page">
435
- <div class="section-header">
436
- <h2 class="section-title">System Diagnostics</h2>
437
- </div>
438
-
439
- <div class="grid-two">
440
- <div class="glass-card">
441
- <h4>Health Status</h4>
442
- <div data-health-info>Checking...</div>
443
- </div>
444
-
445
- <div class="glass-card">
446
- <h4>WebSocket Status</h4>
447
- <div data-ws-info>Checking...</div>
448
- </div>
449
- </div>
450
-
451
- <div class="glass-card" style="margin-top:1.5rem;">
452
- <h4>Request Logs</h4>
453
- <div data-request-logs style="max-height:400px;overflow-y:auto;font-family:monospace;font-size:0.875rem;">
454
- <!-- Populated by JS -->
455
  </div>
456
- </div>
457
- </section>
458
-
459
- <!-- ========== SETTINGS PAGE ========== -->
460
- <section id="page-settings" class="page">
461
- <div class="section-header">
462
- <h2 class="section-title">Settings</h2>
463
- </div>
464
-
465
- <div class="glass-card">
466
- <h4>Display Settings</h4>
467
- <div class="grid-two">
468
- <label class="input-chip">Dark Theme
469
- <div class="toggle">
470
- <input type="checkbox" data-theme-toggle checked />
471
- <span></span>
472
- </div>
473
- </label>
474
- <label class="input-chip">Compact Layout
475
- <div class="toggle">
476
- <input type="checkbox" data-layout-toggle />
477
- <span></span>
478
- </div>
479
- </label>
480
- </div>
481
- </div>
482
-
483
- <div class="glass-card" style="margin-top:1.5rem;">
484
- <h4>Refresh Intervals</h4>
485
- <div class="grid-two">
486
- <label>Market Data (seconds)
487
- <input type="number" min="10" step="5" value="30" data-market-interval />
488
- </label>
489
- <label>News Feed (seconds)
490
- <input type="number" min="30" step="10" value="60" data-news-interval />
491
- </label>
492
- </div>
493
- </div>
494
-
495
- <div class="inline-message inline-info" style="margin-top:1.5rem;">
496
- Settings are stored locally in your browser.
497
- </div>
498
- </section>
499
- </div>
500
- </main>
501
- </div>
502
-
503
- <!-- Load App JS as ES6 Module -->
504
- <script type="module" src="static/js/app.js"></script>
505
  </body>
506
  </html>
 
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>Admin Dashboard - Crypto Monitor</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ :root {
11
+ --primary: #667eea;
12
+ --primary-dark: #5568d3;
13
+ --success: #48bb78;
14
+ --warning: #ed8936;
15
+ --danger: #f56565;
16
+ --bg-dark: #1a202c;
17
+ --bg-card: #2d3748;
18
+ --text-light: #e2e8f0;
19
+ --text-muted: #a0aec0;
20
+ --border: #4a5568;
21
+ }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
25
+ background: var(--bg-dark);
26
+ color: var(--text-light);
27
+ line-height: 1.6;
28
+ }
29
+
30
+ .container {
31
+ max-width: 1400px;
32
+ margin: 0 auto;
33
+ padding: 20px;
34
+ }
35
+
36
+ header {
37
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
38
+ padding: 20px;
39
+ border-radius: 10px;
40
+ margin-bottom: 30px;
41
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
42
+ }
43
+
44
+ header h1 {
45
+ font-size: 28px;
46
+ font-weight: 700;
47
+ margin-bottom: 5px;
48
+ }
49
+
50
+ header .subtitle {
51
+ color: rgba(255, 255, 255, 0.9);
52
+ font-size: 14px;
53
+ }
54
+
55
+ .tabs {
56
+ display: flex;
57
+ gap: 10px;
58
+ margin-bottom: 30px;
59
+ flex-wrap: wrap;
60
+ }
61
+
62
+ .tab-btn {
63
+ padding: 12px 24px;
64
+ background: var(--bg-card);
65
+ border: 2px solid var(--border);
66
+ border-radius: 8px;
67
+ cursor: pointer;
68
+ font-weight: 600;
69
+ color: var(--text-light);
70
+ transition: all 0.3s;
71
+ }
72
+
73
+ .tab-btn:hover {
74
+ background: var(--primary);
75
+ border-color: var(--primary);
76
+ }
77
+
78
+ .tab-btn.active {
79
+ background: var(--primary);
80
+ border-color: var(--primary);
81
+ }
82
+
83
+ .tab-content {
84
+ display: none;
85
+ animation: fadeIn 0.3s;
86
+ }
87
+
88
+ .tab-content.active {
89
+ display: block;
90
+ }
91
+
92
+ @keyframes fadeIn {
93
+ from { opacity: 0; transform: translateY(10px); }
94
+ to { opacity: 1; transform: translateY(0); }
95
+ }
96
+
97
+ .card {
98
+ background: var(--bg-card);
99
+ border-radius: 10px;
100
+ padding: 20px;
101
+ margin-bottom: 20px;
102
+ border: 1px solid var(--border);
103
+ }
104
+
105
+ .card h3 {
106
+ color: var(--primary);
107
+ margin-bottom: 15px;
108
+ font-size: 18px;
109
+ }
110
+
111
+ .stats-grid {
112
+ display: grid;
113
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
114
+ gap: 15px;
115
+ margin-bottom: 20px;
116
+ }
117
+
118
+ .stat-card {
119
+ background: var(--bg-card);
120
+ padding: 20px;
121
+ border-radius: 8px;
122
+ border: 1px solid var(--border);
123
+ }
124
+
125
+ .stat-card .label {
126
+ color: var(--text-muted);
127
+ font-size: 12px;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.5px;
130
+ }
131
+
132
+ .stat-card .value {
133
+ font-size: 32px;
134
+ font-weight: 700;
135
+ color: var(--primary);
136
+ margin: 5px 0;
137
+ }
138
+
139
+ .stat-card .badge {
140
+ display: inline-block;
141
+ padding: 4px 8px;
142
+ border-radius: 4px;
143
+ font-size: 11px;
144
+ font-weight: 600;
145
+ }
146
+
147
+ .badge-success {
148
+ background: var(--success);
149
+ color: white;
150
+ }
151
+
152
+ .badge-warning {
153
+ background: var(--warning);
154
+ color: white;
155
+ }
156
+
157
+ .badge-danger {
158
+ background: var(--danger);
159
+ color: white;
160
+ }
161
+
162
+ .btn {
163
+ padding: 10px 20px;
164
+ border: none;
165
+ border-radius: 6px;
166
+ cursor: pointer;
167
+ font-weight: 600;
168
+ transition: all 0.2s;
169
+ margin-right: 10px;
170
+ margin-bottom: 10px;
171
+ }
172
+
173
+ .btn-primary {
174
+ background: var(--primary);
175
+ color: white;
176
+ }
177
+
178
+ .btn-primary:hover {
179
+ background: var(--primary-dark);
180
+ }
181
+
182
+ .btn-success {
183
+ background: var(--success);
184
+ color: white;
185
+ }
186
+
187
+ .btn-success:hover {
188
+ background: #38a169;
189
+ }
190
+
191
+ .btn-secondary {
192
+ background: var(--bg-card);
193
+ color: var(--text-light);
194
+ border: 1px solid var(--border);
195
+ }
196
+
197
+ .btn-secondary:hover {
198
+ background: var(--border);
199
+ }
200
+
201
+ table {
202
+ width: 100%;
203
+ border-collapse: collapse;
204
+ margin-top: 15px;
205
+ }
206
+
207
+ table thead {
208
+ background: var(--bg-dark);
209
+ }
210
+
211
+ table th {
212
+ padding: 12px;
213
+ text-align: left;
214
+ font-weight: 600;
215
+ font-size: 12px;
216
+ text-transform: uppercase;
217
+ color: var(--text-muted);
218
+ }
219
+
220
+ table td {
221
+ padding: 12px;
222
+ border-top: 1px solid var(--border);
223
+ }
224
+
225
+ table tbody tr:hover {
226
+ background: var(--bg-dark);
227
+ }
228
+
229
+ .status-online {
230
+ color: var(--success);
231
+ }
232
+
233
+ .status-offline {
234
+ color: var(--danger);
235
+ }
236
+
237
+ .status-degraded {
238
+ color: var(--warning);
239
+ }
240
+
241
+ .loading {
242
+ text-align: center;
243
+ padding: 40px;
244
+ color: var(--text-muted);
245
+ }
246
+
247
+ .error-message {
248
+ background: var(--danger);
249
+ color: white;
250
+ padding: 15px;
251
+ border-radius: 8px;
252
+ margin-bottom: 20px;
253
+ }
254
+
255
+ .success-message {
256
+ background: var(--success);
257
+ color: white;
258
+ padding: 15px;
259
+ border-radius: 8px;
260
+ margin-bottom: 20px;
261
+ }
262
+
263
+ .empty-state {
264
+ text-align: center;
265
+ padding: 60px 20px;
266
+ color: var(--text-muted);
267
+ }
268
+
269
+ .empty-state svg {
270
+ width: 64px;
271
+ height: 64px;
272
+ margin-bottom: 20px;
273
+ opacity: 0.3;
274
+ }
275
+
276
+ .filter-bar {
277
+ display: flex;
278
+ gap: 10px;
279
+ margin-bottom: 20px;
280
+ flex-wrap: wrap;
281
+ }
282
+
283
+ select, input {
284
+ padding: 10px;
285
+ border-radius: 6px;
286
+ border: 1px solid var(--border);
287
+ background: var(--bg-dark);
288
+ color: var(--text-light);
289
+ }
290
+
291
+ .log-entry {
292
+ padding: 10px;
293
+ border-left: 3px solid var(--primary);
294
+ margin-bottom: 10px;
295
+ background: var(--bg-dark);
296
+ border-radius: 4px;
297
+ }
298
+
299
+ .log-entry.error {
300
+ border-left-color: var(--danger);
301
+ }
302
+
303
+ .log-timestamp {
304
+ color: var(--text-muted);
305
+ font-size: 12px;
306
+ }
307
+
308
+ pre {
309
+ background: var(--bg-dark);
310
+ padding: 15px;
311
+ border-radius: 6px;
312
+ overflow-x: auto;
313
+ font-size: 13px;
314
+ line-height: 1.4;
315
+ }
316
+
317
+ .model-card {
318
+ background: var(--bg-dark);
319
+ padding: 15px;
320
+ border-radius: 8px;
321
+ margin-bottom: 15px;
322
+ border-left: 4px solid var(--primary);
323
+ }
324
+
325
+ .model-card.valid {
326
+ border-left-color: var(--success);
327
+ }
328
+
329
+ .model-card.conditional {
330
+ border-left-color: var(--warning);
331
+ }
332
+
333
+ .model-card.invalid {
334
+ border-left-color: var(--danger);
335
+ }
336
+
337
+ @media (max-width: 768px) {
338
+ .stats-grid {
339
+ grid-template-columns: 1fr;
340
+ }
341
+
342
+ .tabs {
343
+ flex-direction: column;
344
+ }
345
+
346
+ table {
347
+ font-size: 12px;
348
+ }
349
+
350
+ table th, table td {
351
+ padding: 8px;
352
+ }
353
+ }
354
+ </style>
355
  </head>
356
+ <body>
357
+ <div class="container">
358
+ <header>
359
+ <h1>🚀 Crypto Monitor Admin Dashboard</h1>
360
+ <p class="subtitle">Real-time provider management & system monitoring | NO MOCK DATA</p>
361
+ </header>
362
+
363
+ <div class="tabs">
364
+ <button class="tab-btn active" onclick="switchTab('status')">📊 Status</button>
365
+ <button class="tab-btn" onclick="switchTab('providers')">🔌 Providers</button>
366
+ <button class="tab-btn" onclick="switchTab('market')">💰 Market Data</button>
367
+ <button class="tab-btn" onclick="switchTab('apl')">🤖 APL Scanner</button>
368
+ <button class="tab-btn" onclick="switchTab('hf-models')">🧠 HF Models</button>
369
+ <button class="tab-btn" onclick="switchTab('diagnostics')">🔧 Diagnostics</button>
370
+ <button class="tab-btn" onclick="switchTab('logs')">📝 Logs</button>
371
+ </div>
372
+
373
+ <!-- Status Tab -->
374
+ <div id="tab-status" class="tab-content active">
375
+ <div class="stats-grid" id="global-stats">
376
+ <div class="stat-card">
377
+ <div class="label">System Health</div>
378
+ <div class="value" id="system-health">-</div>
379
+ <span class="badge badge-success" id="health-badge">Healthy</span>
380
+ </div>
381
+ <div class="stat-card">
382
+ <div class="label">Total Providers</div>
383
+ <div class="value" id="total-providers">-</div>
384
+ </div>
385
+ <div class="stat-card">
386
+ <div class="label">Validated</div>
387
+ <div class="value" id="validated-providers">-</div>
388
+ </div>
389
+ <div class="stat-card">
390
+ <div class="label">Database</div>
391
+ <div class="value">✓</div>
392
+ <span class="badge badge-success">Connected</span>
393
+ </div>
394
  </div>
395
+
396
+ <div class="card">
397
+ <h3>Quick Actions</h3>
398
+ <button class="btn btn-primary" onclick="refreshAllData()">🔄 Refresh All</button>
399
+ <button class="btn btn-success" onclick="runAPL()">🤖 Run APL Scan</button>
400
+ <button class="btn btn-secondary" onclick="runDiagnostics()">🔧 Run Diagnostics</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </div>
402
+
403
+ <div class="card">
404
+ <h3>Recent Market Data</h3>
405
+ <div id="quick-market-view"></div>
406
+ </div>
407
+ </div>
408
+
409
+ <!-- Providers Tab -->
410
+ <div id="tab-providers" class="tab-content">
411
+ <div class="card">
412
+ <h3>Providers Management</h3>
413
+ <div class="filter-bar">
414
+ <select id="category-filter" onchange="filterProviders()">
415
+ <option value="">All Categories</option>
416
+ <option value="market_data">Market Data</option>
417
+ <option value="sentiment">Sentiment</option>
418
+ <option value="defi">DeFi</option>
419
+ <option value="exchange">Exchange</option>
420
+ <option value="explorer">Explorer</option>
421
+ <option value="rpc">RPC</option>
422
+ <option value="news">News</option>
423
+ </select>
424
+ <button class="btn btn-secondary" onclick="loadProviders()">🔄 Refresh</button>
425
  </div>
426
+ <div id="providers-table"></div>
427
+ </div>
428
+ </div>
429
+
430
+ <!-- Market Data Tab -->
431
+ <div id="tab-market" class="tab-content">
432
+ <div class="card">
433
+ <h3>Live Market Data</h3>
434
+ <button class="btn btn-primary" onclick="loadMarketData()">🔄 Refresh Prices</button>
435
+ <div id="market-data-container"></div>
436
+ </div>
437
+
438
+ <div class="card">
439
+ <h3>Sentiment Analysis</h3>
440
+ <div id="sentiment-data"></div>
441
+ </div>
442
+
443
+ <div class="card">
444
+ <h3>Trending Coins</h3>
445
+ <div id="trending-coins"></div>
446
+ </div>
447
+ </div>
448
+
449
+ <!-- APL Tab -->
450
+ <div id="tab-apl" class="tab-content">
451
+ <div class="card">
452
+ <h3>Auto Provider Loader (APL)</h3>
453
+ <p style="color: var(--text-muted); margin-bottom: 20px;">
454
+ APL automatically discovers, validates, and integrates cryptocurrency data providers.
455
+ All validations use REAL API calls - NO MOCK DATA.
456
+ </p>
457
+
458
+ <button class="btn btn-success" onclick="runAPL()" id="apl-run-btn">
459
+ 🤖 Run APL Scan
460
+ </button>
461
+ <button class="btn btn-secondary" onclick="loadAPLReport()">📊 View Last Report</button>
462
+
463
+ <div id="apl-status" style="margin-top: 20px;"></div>
464
+ </div>
465
+
466
+ <div class="card">
467
+ <h3>APL Summary Statistics</h3>
468
+ <div id="apl-summary"></div>
469
+ </div>
470
+
471
+ <div class="card">
472
+ <h3>APL Output</h3>
473
+ <pre id="apl-output" style="max-height: 400px; overflow-y: auto;">No output yet. Click "Run APL Scan" to start.</pre>
474
+ </div>
475
+ </div>
476
+
477
+ <!-- HF Models Tab -->
478
+ <div id="tab-hf-models" class="tab-content">
479
+ <div class="card">
480
+ <h3>Hugging Face Models</h3>
481
+ <p style="color: var(--text-muted); margin-bottom: 20px;">
482
+ HuggingFace models validated by APL for crypto sentiment analysis and NLP tasks.
483
+ </p>
484
+ <button class="btn btn-primary" onclick="loadHFModels()">🔄 Refresh Models</button>
485
+ <div id="hf-models-container"></div>
486
+ </div>
487
+
488
+ <div class="card">
489
+ <h3>HF Services Health</h3>
490
+ <div id="hf-health"></div>
491
+ </div>
492
+ </div>
493
+
494
+ <!-- Diagnostics Tab -->
495
+ <div id="tab-diagnostics" class="tab-content">
496
+ <div class="card">
497
+ <h3>System Diagnostics</h3>
498
+ <button class="btn btn-primary" onclick="runDiagnostics(true)">🔧 Run with Auto-Fix</button>
499
+ <button class="btn btn-secondary" onclick="runDiagnostics(false)">🔍 Run Scan Only</button>
500
+ <button class="btn btn-secondary" onclick="loadLastDiagnostics()">📋 View Last Results</button>
501
+
502
+ <div id="diagnostics-results" style="margin-top: 20px;"></div>
503
+ </div>
504
+ </div>
505
+
506
+ <!-- Logs Tab -->
507
+ <div id="tab-logs" class="tab-content">
508
+ <div class="card">
509
+ <h3>System Logs</h3>
510
+ <button class="btn btn-primary" onclick="loadRecentLogs()">🔄 Refresh</button>
511
+ <button class="btn btn-danger" onclick="loadErrorLogs()">❌ Errors Only</button>
512
+
513
+ <div id="logs-container" style="margin-top: 20px;"></div>
514
+ </div>
515
+ </div>
516
+ </div>
517
 
518
+ <script src="/static/js/api-client.js"></script>
519
+ <script>
520
+ // Tab switching
521
+ function switchTab(tabName) {
522
+ // Hide all tabs
523
+ document.querySelectorAll('.tab-content').forEach(tab => {
524
+ tab.classList.remove('active');
525
+ });
526
+ document.querySelectorAll('.tab-btn').forEach(btn => {
527
+ btn.classList.remove('active');
528
+ });
529
+
530
+ // Show selected tab
531
+ document.getElementById(`tab-${tabName}`).classList.add('active');
532
+ event.target.classList.add('active');
533
+
534
+ // Load data for tab
535
+ switch(tabName) {
536
+ case 'status':
537
+ loadGlobalStatus();
538
+ break;
539
+ case 'providers':
540
+ loadProviders();
541
+ break;
542
+ case 'market':
543
+ loadMarketData();
544
+ loadSentiment();
545
+ loadTrending();
546
+ break;
547
+ case 'apl':
548
+ loadAPLSummary();
549
+ break;
550
+ case 'hf-models':
551
+ loadHFModels();
552
+ loadHFHealth();
553
+ break;
554
+ case 'diagnostics':
555
+ loadLastDiagnostics();
556
+ break;
557
+ case 'logs':
558
+ loadRecentLogs();
559
+ break;
560
+ }
561
+ }
562
+
563
+ // Global Status
564
+ async function loadGlobalStatus() {
565
+ try {
566
+ const [status, stats] = await Promise.all([
567
+ apiClient.get('/api/status'),
568
+ apiClient.get('/api/stats')
569
+ ]);
570
+
571
+ document.getElementById('system-health').textContent = status.system_health.toUpperCase();
572
+ document.getElementById('total-providers').textContent = status.total_providers;
573
+ document.getElementById('validated-providers').textContent = status.validated_providers;
574
+
575
+ // Quick market view
576
+ const market = await apiClient.get('/api/market');
577
+ let marketHTML = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">';
578
+ market.cryptocurrencies.forEach(coin => {
579
+ const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
580
+ marketHTML += `
581
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
582
+ <div style="font-weight: 600;">${coin.name} (${coin.symbol})</div>
583
+ <div style="font-size: 24px; margin: 10px 0;">$${coin.price.toLocaleString()}</div>
584
+ <div class="${changeClass}">${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%</div>
585
  </div>
586
+ `;
587
+ });
588
+ marketHTML += '</div>';
589
+ document.getElementById('quick-market-view').innerHTML = marketHTML;
590
+
591
+ } catch (error) {
592
+ console.error('Error loading global status:', error);
593
+ showError('Failed to load global status');
594
+ }
595
+ }
596
+
597
+ // Providers
598
+ async function loadProviders() {
599
+ try {
600
+ const response = await apiClient.get('/api/providers');
601
+ const providers = response.providers;
602
+
603
+ if (providers.length === 0) {
604
+ document.getElementById('providers-table').innerHTML = '<div class="empty-state">No providers found. Run APL scan to discover providers.</div>';
605
+ return;
606
+ }
607
+
608
+ let html = `
609
+ <table>
610
+ <thead>
611
+ <tr>
612
+ <th>Provider ID</th>
613
+ <th>Name</th>
614
+ <th>Category</th>
615
+ <th>Type</th>
616
+ <th>Status</th>
617
+ <th>Response Time</th>
618
+ </tr>
619
+ </thead>
620
+ <tbody>
621
+ `;
622
+
623
+ providers.forEach(p => {
624
+ const statusClass = p.status === 'validated' ? 'status-online' : 'status-degraded';
625
+ html += `
626
+ <tr>
627
+ <td><code>${p.provider_id}</code></td>
628
+ <td>${p.name}</td>
629
+ <td>${p.category}</td>
630
+ <td>${p.type}</td>
631
+ <td class="${statusClass}">${p.status}</td>
632
+ <td>${p.response_time_ms ? p.response_time_ms.toFixed(0) + 'ms' : 'N/A'}</td>
633
+ </tr>
634
+ `;
635
+ });
636
+
637
+ html += '</tbody></table>';
638
+ document.getElementById('providers-table').innerHTML = html;
639
+
640
+ } catch (error) {
641
+ console.error('Error loading providers:', error);
642
+ showError('Failed to load providers');
643
+ }
644
+ }
645
+
646
+ function filterProviders() {
647
+ // Would filter the providers table
648
+ loadProviders();
649
+ }
650
+
651
+ // Market Data
652
+ async function loadMarketData() {
653
+ try {
654
+ const data = await apiClient.get('/api/market');
655
+
656
+ let html = '<table><thead><tr><th>Rank</th><th>Coin</th><th>Price</th><th>24h Change</th><th>Market Cap</th><th>Volume 24h</th></tr></thead><tbody>';
657
+
658
+ data.cryptocurrencies.forEach(coin => {
659
+ const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
660
+ html += `
661
+ <tr>
662
+ <td>${coin.rank}</td>
663
+ <td><strong>${coin.name}</strong> (${coin.symbol})</td>
664
+ <td>$${coin.price.toLocaleString()}</td>
665
+ <td class="${changeClass}">${coin.change_24h >= 0 ? '+' : ''}${coin.change_24h.toFixed(2)}%</td>
666
+ <td>$${(coin.market_cap / 1e9).toFixed(2)}B</td>
667
+ <td>$${(coin.volume_24h / 1e9).toFixed(2)}B</td>
668
+ </tr>
669
+ `;
670
+ });
671
+
672
+ html += '</tbody></table>';
673
+ html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
674
+
675
+ document.getElementById('market-data-container').innerHTML = html;
676
+ } catch (error) {
677
+ console.error('Error loading market data:', error);
678
+ document.getElementById('market-data-container').innerHTML = '<div class="error-message">Failed to load market data: ' + error.message + '</div>';
679
+ }
680
+ }
681
+
682
+ async function loadSentiment() {
683
+ try {
684
+ const data = await apiClient.get('/api/sentiment');
685
+
686
+ const fngValue = data.fear_greed_index;
687
+ let color = '--success';
688
+ if (fngValue < 30) color = '--danger';
689
+ else if (fngValue < 50) color = '--warning';
690
+
691
+ document.getElementById('sentiment-data').innerHTML = `
692
+ <div style="text-align: center; padding: 20px;">
693
+ <div style="font-size: 64px; color: var(${color}); font-weight: 700;">${fngValue}</div>
694
+ <div style="font-size: 24px; margin: 10px 0;">${data.fear_greed_label}</div>
695
+ <p style="color: var(--text-muted);">Source: ${data.source}</p>
696
+ </div>
697
+ `;
698
+ } catch (error) {
699
+ console.error('Error loading sentiment:', error);
700
+ document.getElementById('sentiment-data').innerHTML = '<div class="error-message">Failed to load sentiment: ' + error.message + '</div>';
701
+ }
702
+ }
703
+
704
+ async function loadTrending() {
705
+ try {
706
+ const data = await apiClient.get('/api/trending');
707
+
708
+ if (data.trending.length === 0) {
709
+ document.getElementById('trending-coins').innerHTML = '<div class="empty-state">No trending coins available</div>';
710
+ return;
711
+ }
712
+
713
+ let html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">';
714
+ data.trending.forEach(coin => {
715
+ html += `
716
+ <div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
717
+ <div style="font-weight: 600;">${coin.name}</div>
718
+ <div style="color: var(--text-muted);">${coin.symbol}</div>
719
+ ${coin.market_cap_rank ? `<div style="margin-top: 10px;">Rank: #${coin.market_cap_rank}</div>` : ''}
720
  </div>
721
+ `;
722
+ });
723
+ html += '</div>';
724
+ html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
725
+
726
+ document.getElementById('trending-coins').innerHTML = html;
727
+ } catch (error) {
728
+ console.error('Error loading trending:', error);
729
+ document.getElementById('trending-coins').innerHTML = '<div class="error-message">Failed to load trending: ' + error.message + '</div>';
730
+ }
731
+ }
732
+
733
+ // APL Functions
734
+ async function runAPL() {
735
+ const btn = document.getElementById('apl-run-btn');
736
+ btn.disabled = true;
737
+ btn.textContent = '⏳ Running APL Scan...';
738
+
739
+ document.getElementById('apl-status').innerHTML = '<div class="loading">Running APL scan... This may take 1-2 minutes...</div>';
740
+ document.getElementById('apl-output').textContent = 'Executing APL scan...';
741
+
742
+ try {
743
+ const result = await apiClient.post('/api/apl/run');
744
+
745
+ if (result.status === 'completed') {
746
+ document.getElementById('apl-status').innerHTML = `
747
+ <div class="success-message">
748
+ ✓ APL scan completed successfully!<br>
749
+ Providers count: ${result.providers_count}<br>
750
+ Time: ${result.timestamp}
751
  </div>
752
+ `;
753
+ document.getElementById('apl-output').textContent = result.stdout || 'Scan completed.';
754
+
755
+ // Reload summary
756
+ await loadAPLSummary();
757
+
758
+ } else {
759
+ document.getElementById('apl-status').innerHTML = `<div class="error-message">APL scan ${result.status}: ${result.message || 'Unknown error'}</div>`;
760
+ document.getElementById('apl-output').textContent = result.stdout || 'No output';
761
+ }
762
+ } catch (error) {
763
+ console.error('Error running APL:', error);
764
+ document.getElementById('apl-status').innerHTML = '<div class="error-message">Failed to run APL: ' + error.message + '</div>';
765
+ } finally {
766
+ btn.disabled = false;
767
+ btn.textContent = '🤖 Run APL Scan';
768
+ }
769
+ }
770
+
771
+ async function loadAPLSummary() {
772
+ try {
773
+ const summary = await apiClient.get('/api/apl/summary');
774
+
775
+ if (summary.status === 'not_available') {
776
+ document.getElementById('apl-summary').innerHTML = '<div class="empty-state">No APL report available. Run APL scan first.</div>';
777
+ return;
778
+ }
779
+
780
+ document.getElementById('apl-summary').innerHTML = `
781
+ <div class="stats-grid">
782
+ <div class="stat-card">
783
+ <div class="label">HTTP Candidates</div>
784
+ <div class="value">${summary.http_candidates}</div>
785
+ <span class="badge badge-success">Valid: ${summary.http_valid}</span>
786
  </div>
787
+ <div class="stat-card">
788
+ <div class="label">HTTP Invalid</div>
789
+ <div class="value">${summary.http_invalid}</div>
790
+ <span class="badge badge-warning">Conditional: ${summary.http_conditional}</span>
 
 
 
 
 
 
 
 
 
791
  </div>
792
+ <div class="stat-card">
793
+ <div class="label">HF Models</div>
794
+ <div class="value">${summary.hf_candidates}</div>
795
+ <span class="badge badge-success">Valid: ${summary.hf_valid}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
  </div>
797
+ <div class="stat-card">
798
+ <div class="label">Total Active</div>
799
+ <div class="value">${summary.total_active}</div>
800
+ <span class="badge badge-success">Providers</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  </div>
802
  </div>
803
+ <p style="margin-top: 15px; color: var(--text-muted);">Last updated: ${summary.timestamp}</p>
804
+ `;
805
+ } catch (error) {
806
+ console.error('Error loading APL summary:', error);
807
+ document.getElementById('apl-summary').innerHTML = '<div class="error-message">Failed to load APL summary</div>';
808
+ }
809
+ }
810
+
811
+ async function loadAPLReport() {
812
+ try {
813
+ const report = await apiClient.get('/api/apl/report');
814
+
815
+ if (report.status === 'not_available') {
816
+ showError('APL report not available. Run APL scan first.');
817
+ return;
818
+ }
819
+
820
+ document.getElementById('apl-output').textContent = JSON.stringify(report, null, 2);
821
+ } catch (error) {
822
+ console.error('Error loading APL report:', error);
823
+ showError('Failed to load APL report');
824
+ }
825
+ }
826
+
827
+ // HF Models
828
+ async function loadHFModels() {
829
+ try {
830
+ const response = await apiClient.get('/api/hf/models');
831
+
832
+ if (response.count === 0) {
833
+ document.getElementById('hf-models-container').innerHTML = '<div class="empty-state">No HF models found. Run APL scan to discover models.</div>';
834
+ return;
835
+ }
836
+
837
+ let html = '';
838
+ response.models.forEach(model => {
839
+ const statusClass = model.status === 'VALID' ? 'valid' : model.status === 'CONDITIONALLY_AVAILABLE' ? 'conditional' : 'invalid';
840
+ html += `
841
+ <div class="model-card ${statusClass}">
842
+ <div style="font-weight: 600; margin-bottom: 5px;">${model.provider_name}</div>
843
+ <div style="color: var(--text-muted); font-size: 13px;">${model.provider_id}</div>
844
+ <div style="margin-top: 10px;">
845
+ <span class="badge badge-${statusClass === 'valid' ? 'success' : statusClass === 'conditional' ? 'warning' : 'danger'}">${model.status}</span>
 
846
  </div>
847
+ ${model.error_reason ? `<div style="margin-top: 10px; color: var(--warning);">${model.error_reason}</div>` : ''}
848
  </div>
849
+ `;
850
+ });
851
+
852
+ document.getElementById('hf-models-container').innerHTML = html;
853
+ } catch (error) {
854
+ console.error('Error loading HF models:', error);
855
+ document.getElementById('hf-models-container').innerHTML = '<div class="error-message">Failed to load HF models</div>';
856
+ }
857
+ }
858
+
859
+ async function loadHFHealth() {
860
+ try {
861
+ const health = await apiClient.get('/api/hf/health');
862
+
863
+ const statusClass = health.ok ? 'status-online' : 'status-offline';
864
+ document.getElementById('hf-health').innerHTML = `
865
+ <div style="padding: 20px; background: var(--bg-dark); border-radius: 8px;">
866
+ <div class="${statusClass}" style="font-size: 24px; font-weight: 700;">${health.ok ? '✓ Healthy' : '✗ Unhealthy'}</div>
867
+ ${health.ok ? `
868
+ <div style="margin-top: 15px;">
869
+ <div>Models: ${health.counts.models}</div>
870
+ <div>Datasets: ${health.counts.datasets}</div>
871
+ <div>Last refresh: ${new Date(health.last_refresh_epoch * 1000).toLocaleString()}</div>
872
  </div>
873
+ ` : `
874
+ <div style="margin-top: 15px; color: var(--danger);">Error: ${health.error || health.fail_reason}</div>
875
+ `}
876
+ </div>
877
+ `;
878
+ } catch (error) {
879
+ console.error('Error loading HF health:', error);
880
+ document.getElementById('hf-health').innerHTML = '<div class="error-message">Failed to load HF health</div>';
881
+ }
882
+ }
883
+
884
+ // Diagnostics
885
+ async function runDiagnostics(autoFix = false) {
886
+ try {
887
+ const result = await apiClient.post('/api/diagnostics/run?auto_fix=' + autoFix);
888
+
889
+ let html = `
890
+ <div class="success-message">
891
+ ✓ Diagnostics completed<br>
892
+ Issues found: ${result.issues_found}<br>
893
+ Time: ${result.timestamp}
894
+ </div>
895
+ `;
896
+
897
+ if (result.issues.length > 0) {
898
+ html += '<div style="margin-top: 20px;"><h4>Issues Found:</h4>';
899
+ result.issues.forEach(issue => {
900
+ html += `<div class="log-entry error">${issue.type}: ${issue.message}</div>`;
901
+ });
902
+ html += '</div>';
903
+ }
904
+
905
+ if (result.fixes_applied.length > 0) {
906
+ html += '<div style="margin-top: 20px;"><h4>Fixes Applied:</h4>';
907
+ result.fixes_applied.forEach(fix => {
908
+ html += `<div class="log-entry">${fix}</div>`;
909
+ });
910
+ html += '</div>';
911
+ }
912
+
913
+ document.getElementById('diagnostics-results').innerHTML = html;
914
+ } catch (error) {
915
+ console.error('Error running diagnostics:', error);
916
+ showError('Failed to run diagnostics');
917
+ }
918
+ }
919
+
920
+ async function loadLastDiagnostics() {
921
+ try {
922
+ const result = await apiClient.get('/api/diagnostics/last');
923
+
924
+ if (result.status === 'no_previous_run') {
925
+ document.getElementById('diagnostics-results').innerHTML = '<div class="empty-state">No previous diagnostics run found</div>';
926
+ return;
927
+ }
928
+
929
+ // Display last diagnostics
930
+ document.getElementById('diagnostics-results').innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
931
+ } catch (error) {
932
+ console.error('Error loading diagnostics:', error);
933
+ showError('Failed to load diagnostics');
934
+ }
935
+ }
936
+
937
+ // Logs
938
+ async function loadRecentLogs() {
939
+ try {
940
+ const response = await apiClient.get('/api/logs/recent');
941
+
942
+ if (response.count === 0) {
943
+ document.getElementById('logs-container').innerHTML = '<div class="empty-state">No logs available</div>';
944
+ return;
945
+ }
946
+
947
+ let html = '';
948
+ response.logs.forEach(log => {
949
+ const className = log.level === 'ERROR' ? 'error' : '';
950
+ html += `
951
+ <div class="log-entry ${className}">
952
+ <div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
953
+ <div>${log.message || JSON.stringify(log)}</div>
954
  </div>
955
+ `;
956
+ });
957
+
958
+ document.getElementById('logs-container').innerHTML = html;
959
+ } catch (error) {
960
+ console.error('Error loading logs:', error);
961
+ document.getElementById('logs-container').innerHTML = '<div class="error-message">Failed to load logs</div>';
962
+ }
963
+ }
964
+
965
+ async function loadErrorLogs() {
966
+ try {
967
+ const response = await apiClient.get('/api/logs/errors');
968
+
969
+ if (response.count === 0) {
970
+ document.getElementById('logs-container').innerHTML = '<div class="empty-state">No error logs found</div>';
971
+ return;
972
+ }
973
+
974
+ let html = '';
975
+ response.errors.forEach(log => {
976
+ html += `
977
+ <div class="log-entry error">
978
+ <div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
979
+ <div>${log.message || JSON.stringify(log)}</div>
980
  </div>
981
+ `;
982
+ });
983
+
984
+ document.getElementById('logs-container').innerHTML = html;
985
+ } catch (error) {
986
+ console.error('Error loading error logs:', error);
987
+ showError('Failed to load error logs');
988
+ }
989
+ }
990
+
991
+ // Utility functions
992
+ function showError(message) {
993
+ alert('Error: ' + message);
994
+ }
995
+
996
+ function refreshAllData() {
997
+ loadGlobalStatus();
998
+ loadProviders();
999
+ loadAPLSummary();
1000
+ }
1001
+
1002
+ // Auto-refresh every 30 seconds
1003
+ setInterval(() => {
1004
+ const activeTab = document.querySelector('.tab-content.active').id;
1005
+ if (activeTab === 'tab-status') {
1006
+ loadGlobalStatus();
1007
+ }
1008
+ }, 30000);
1009
+
1010
+ // Load initial data
1011
+ window.addEventListener('DOMContentLoaded', () => {
1012
+ console.log('✓ Admin Dashboard Loaded - Real Data Only');
1013
+ loadGlobalStatus();
1014
+ });
1015
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  </body>
1017
  </html>
config.js CHANGED
@@ -1,366 +1,146 @@
1
  /**
2
- * ═══════════════════════════════════════════════════════════════════
3
- * CONFIGURATION FILE
4
- * Dashboard Settings - Easy Customization
5
- * ═══════════════════════════════════════════════════════════════════
6
  */
7
 
8
- // 🔧 Main Backend Settings
9
- window.DASHBOARD_CONFIG = {
10
-
11
- // ═══════════════════════════════════════════════════════════════
12
- // API and WebSocket URLs
13
- // ═══════════════════════════════════════════════════════════════
14
-
15
- BACKEND_URL: window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space',
16
- WS_URL: (window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space').replace('http://', 'ws://').replace('https://', 'wss://') + '/ws',
17
-
18
- // ⏱️ Update Timing (milliseconds)
19
- UPDATE_INTERVAL: 30000, // Every 30 seconds
20
- CACHE_TTL: 60000, // 1 minute
21
- HEARTBEAT_INTERVAL: 30000, // 30 seconds
22
-
23
- // 🔄 Reconnection Settings
24
- MAX_RECONNECT_ATTEMPTS: 5,
25
- RECONNECT_DELAY: 3000, // 3 seconds
26
-
27
- // ═══════════════════════════════════════════════════════════════
28
- // Display Settings
29
- // ═══════════════════════════════════════════════════════════════
30
-
31
- // Number of items to display
32
- MAX_COINS_DISPLAY: 20, // Number of coins in table
33
- MAX_NEWS_DISPLAY: 20, // Number of news items
34
- MAX_TRENDING_DISPLAY: 10, // Number of trending items
35
-
36
- // Table settings
37
- TABLE_ROWS_PER_PAGE: 10,
38
-
39
- // ═══════════════════════════════════════════════════════════════
40
- // Chart Settings
41
- // ═══════════════════════════════════════════════════════════════
42
-
43
- CHART: {
44
- DEFAULT_SYMBOL: 'BTCUSDT',
45
- DEFAULT_INTERVAL: '1h',
46
- AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
47
- THEME: 'dark',
48
- },
49
-
50
- // ═══════════════════════════════════════════════════════════════
51
- // AI Settings
52
- // ═══════════════════════════════════════════════════════════════
53
-
54
- AI: {
55
- ENABLE_SENTIMENT: true,
56
- ENABLE_NEWS_SUMMARY: true,
57
- ENABLE_PRICE_PREDICTION: false, // Currently disabled
58
- ENABLE_PATTERN_DETECTION: false, // Currently disabled
59
- },
60
-
61
- // ═══════════════════════════════════════════════════════════════
62
- // Notification Settings
63
- // ═══════════════════════════════════════════════════════════════
64
-
65
- NOTIFICATIONS: {
66
- ENABLE: true,
67
- SHOW_PRICE_ALERTS: true,
68
- SHOW_NEWS_ALERTS: true,
69
- AUTO_DISMISS_TIME: 5000, // 5 seconds
70
- },
71
-
72
- // ═══════════════════════════════════════════════════════════════
73
- // UI Settings
74
- // ═══════════════════════════════════════════════════════════════
75
-
76
- UI: {
77
- DEFAULT_THEME: 'dark', // 'dark' or 'light'
78
- ENABLE_ANIMATIONS: true,
79
- ENABLE_SOUNDS: false,
80
- LANGUAGE: 'en', // 'en' or 'fa'
81
- RTL: false,
82
- },
83
-
84
- // ═══════════════════════════════════════════════════════════════
85
- // Debug Settings
86
- // ═══════════════════════════════════════════════════════════════
87
-
88
- DEBUG: {
89
- ENABLE_CONSOLE_LOGS: true,
90
- ENABLE_PERFORMANCE_MONITORING: true,
91
- SHOW_API_REQUESTS: true,
92
- SHOW_WS_MESSAGES: false,
93
- },
94
-
95
- // ═══════════════════════════════════════════════════════════════
96
- // Default Filters and Sorting
97
- // ═══════════════════════════════════════════════════════════════
98
-
99
- FILTERS: {
100
- DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
101
- DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
102
- DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
103
- SORT_ORDER: 'desc', // 'asc' or 'desc'
104
- },
105
-
106
- // ═══════════════════════════════════════════════════════════════
107
- // API Endpoints (Optional - if your backend differs)
108
- // ═══════════════════════════════════════════════════════════════
109
-
110
- ENDPOINTS: {
111
- HEALTH: '/api/health',
112
- MARKET: '/api/market/stats',
113
- MARKET_PRICES: '/api/market/prices',
114
- COINS_TOP: '/api/coins/top',
115
- COIN_DETAILS: '/api/coins',
116
- TRENDING: '/api/trending',
117
- SENTIMENT: '/api/sentiment',
118
- SENTIMENT_ANALYZE: '/api/sentiment/analyze',
119
- NEWS: '/api/news/latest',
120
- NEWS_SUMMARIZE: '/api/news/summarize',
121
- STATS: '/api/stats',
122
- PROVIDERS: '/api/providers',
123
- PROVIDER_STATUS: '/api/providers/status',
124
- CHART_HISTORY: '/api/charts/price',
125
- CHART_ANALYZE: '/api/charts/analyze',
126
- OHLCV: '/api/ohlcv',
127
- QUERY: '/api/query',
128
- DATASETS: '/api/datasets/list',
129
- MODELS: '/api/models/list',
130
- HF_HEALTH: '/api/hf/health',
131
- HF_REGISTRY: '/api/hf/registry',
132
- SYSTEM_STATUS: '/api/system/status',
133
- SYSTEM_CONFIG: '/api/system/config',
134
- CATEGORIES: '/api/categories',
135
- RATE_LIMITS: '/api/rate-limits',
136
- LOGS: '/api/logs',
137
- ALERTS: '/api/alerts',
138
- },
139
-
140
- // ═══════════════════════════════════════════════════════════════
141
- // WebSocket Events
142
- // ═══════════════════════════════════════════════════════════════
143
-
144
- WS_EVENTS: {
145
- MARKET_UPDATE: 'market_update',
146
- SENTIMENT_UPDATE: 'sentiment_update',
147
- NEWS_UPDATE: 'news_update',
148
- STATS_UPDATE: 'stats_update',
149
- PRICE_UPDATE: 'price_update',
150
- API_UPDATE: 'api_update',
151
- STATUS_UPDATE: 'status_update',
152
- SCHEDULE_UPDATE: 'schedule_update',
153
- CONNECTED: 'connected',
154
- DISCONNECTED: 'disconnected',
155
- },
156
 
157
- // ═══════════════════════════════════════════════════════════════
158
- // Display Formats
159
- // ═══════════════════════════════════════════════════════════════
 
160
 
161
- FORMATS: {
162
- CURRENCY: {
163
- LOCALE: 'en-US',
164
- STYLE: 'currency',
165
- CURRENCY: 'USD',
166
- },
167
- DATE: {
168
- LOCALE: 'en-US',
169
- OPTIONS: {
170
- year: 'numeric',
171
- month: 'long',
172
- day: 'numeric',
173
- hour: '2-digit',
174
- minute: '2-digit',
175
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  },
177
- },
178
-
179
- // ═══════════════════════════════════════════════════════════════
180
- // Rate Limiting
181
- // ═══════════════════════════════════════════════════════════════
182
-
183
- RATE_LIMITS: {
184
- API_REQUESTS_PER_MINUTE: 60,
185
- SEARCH_DEBOUNCE_MS: 300,
186
- },
187
-
188
- // ═══════════════════════════════════════════════════════════════
189
- // Storage Settings
190
- // ═══════════════════════════════════════════════════════════════
191
 
192
- STORAGE: {
193
- USE_LOCAL_STORAGE: true,
194
- SAVE_PREFERENCES: true,
195
- STORAGE_PREFIX: 'hts_dashboard_',
196
- },
197
- };
198
-
199
- // ═══════════════════════════════════════════════════════════════════
200
- // Predefined Profiles
201
- // ═══════════════════════════════════════════════════════════════════
202
-
203
- window.DASHBOARD_PROFILES = {
204
-
205
- // High Performance Profile
206
- HIGH_PERFORMANCE: {
207
- UPDATE_INTERVAL: 15000, // Faster updates
208
- CACHE_TTL: 30000, // Shorter cache
209
- ENABLE_ANIMATIONS: false, // No animations
210
- MAX_COINS_DISPLAY: 50,
211
- },
212
-
213
- // Data Saver Profile
214
- DATA_SAVER: {
215
- UPDATE_INTERVAL: 60000, // Less frequent updates
216
- CACHE_TTL: 300000, // Longer cache (5 minutes)
217
- MAX_COINS_DISPLAY: 10,
218
- MAX_NEWS_DISPLAY: 10,
219
- },
220
-
221
- // Presentation Profile
222
- PRESENTATION: {
223
- ENABLE_ANIMATIONS: true,
224
- UPDATE_INTERVAL: 20000,
225
- SHOW_API_REQUESTS: false,
226
- ENABLE_CONSOLE_LOGS: false,
227
- },
228
-
229
- // Development Profile
230
- DEVELOPMENT: {
231
- DEBUG: {
232
- ENABLE_CONSOLE_LOGS: true,
233
- ENABLE_PERFORMANCE_MONITORING: true,
234
- SHOW_API_REQUESTS: true,
235
- SHOW_WS_MESSAGES: true,
236
  },
237
- UPDATE_INTERVAL: 10000,
238
- },
239
- };
240
-
241
- // ═══════════════════════════════════════════════════════════════════
242
- // Helper Function to Change Profile
243
- // ═══════════════════════════════════════════════════════════════════
244
-
245
- window.applyDashboardProfile = function (profileName) {
246
- if (window.DASHBOARD_PROFILES[profileName]) {
247
- const profile = window.DASHBOARD_PROFILES[profileName];
248
- Object.assign(window.DASHBOARD_CONFIG, profile);
249
- console.log(`✅ Profile "${profileName}" applied`);
250
-
251
- // Reload application with new settings
252
- if (window.app) {
253
- window.app.destroy();
254
- window.app = new DashboardApp();
255
- window.app.init();
256
- }
257
- } else {
258
- console.error(`❌ Profile "${profileName}" not found`);
259
- }
260
- };
261
-
262
- // ═══════════════════════════════════════════════════════════════════
263
- // Helper Function to Change Backend URL
264
- // ═══════════════════════════════════════════════════════════════════
265
-
266
- window.changeBackendURL = function (httpUrl, wsUrl) {
267
- window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
268
- window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
269
-
270
- console.log('✅ Backend URL changed:');
271
- console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
272
- console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
273
-
274
- // Reload application
275
- if (window.app) {
276
- window.app.destroy();
277
- window.app = new DashboardApp();
278
- window.app.init();
279
- }
280
- };
281
-
282
- // ═══════════════════════════════════════════════════════════════════
283
- // Save Settings to LocalStorage
284
- // ═══════════════════════════════════════════════════════════════════
285
 
286
- window.saveConfig = function () {
287
- if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
288
- try {
289
- const configString = JSON.stringify(window.DASHBOARD_CONFIG);
290
- localStorage.setItem(
291
- window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
292
- configString
293
- );
294
- console.log('✅ Settings saved');
295
- } catch (error) {
296
- console.error('❌ Error saving settings:', error);
297
- }
298
- }
299
- };
300
 
301
- // ═══════════════════════════════════════════════════════════════════
302
- // Load Settings from LocalStorage
303
- // ═══════════════════════════════════════════════════════════════════
304
 
305
- window.loadConfig = function () {
306
- if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
307
- try {
308
- const configString = localStorage.getItem(
309
- window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config'
310
- );
311
- if (configString) {
312
- const savedConfig = JSON.parse(configString);
313
- Object.assign(window.DASHBOARD_CONFIG, savedConfig);
314
- console.log('✅ Settings loaded');
 
315
  }
316
- } catch (error) {
317
- console.error('❌ Error loading settings:', error);
318
- }
319
- }
320
- };
321
 
322
- // ══════════════════════════════════════════════════════��════════════
323
- // Auto-load Settings on Page Load
324
- // ═══════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
325
 
326
- if (document.readyState === 'loading') {
327
- document.addEventListener('DOMContentLoaded', () => {
328
- window.loadConfig();
329
- });
330
- } else {
331
- window.loadConfig();
332
  }
333
 
334
- // ═══════════════════════════════════════════════════════════════════
335
- // Console Usage Guide
336
- // ═══════════════════════════════════════════════════════════════════
337
-
338
- console.log(`
339
- ╔═══════════════════════════════════════════════════════════════╗
340
- ║ HTS CRYPTO DASHBOARD - CONFIGURATION ║
341
- ╚═══════════════════════════════════════════════════════════════╝
342
-
343
- 📋 Available Commands:
344
-
345
- 1. Change Profile:
346
- applyDashboardProfile('HIGH_PERFORMANCE')
347
- applyDashboardProfile('DATA_SAVER')
348
- applyDashboardProfile('PRESENTATION')
349
- applyDashboardProfile('DEVELOPMENT')
350
-
351
- 2. Change Backend:
352
- changeBackendURL('https://your-backend.com')
353
-
354
- 3. Save/Load Settings:
355
- saveConfig()
356
- loadConfig()
357
-
358
- 4. View Current Settings:
359
- console.log(DASHBOARD_CONFIG)
360
-
361
- 5. Manual Settings Change:
362
- DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
363
- saveConfig()
364
-
365
- ═══════════════════════════════════════════════════════════════════
366
- `);
 
1
  /**
2
+ * API Configuration for Crypto API Monitoring System
3
+ * Automatically detects environment (localhost, HuggingFace Spaces, or custom deployment)
 
 
4
  */
5
 
6
+ const CONFIG = (() => {
7
+ // Detect if running on HuggingFace Spaces
8
+ const isHuggingFaceSpaces = window.location.hostname.includes('hf.space') ||
9
+ window.location.hostname.includes('huggingface.co');
10
+
11
+ // Detect if running locally
12
+ const isLocalhost = window.location.hostname === 'localhost' ||
13
+ window.location.hostname === '127.0.0.1' ||
14
+ window.location.hostname === '';
15
+
16
+ // Get base API URL based on environment
17
+ const getApiBaseUrl = () => {
18
+ // If running on HuggingFace Spaces, use relative URLs
19
+ if (isHuggingFaceSpaces) {
20
+ return window.location.origin;
21
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ // If running locally, use localhost with port 7860
24
+ if (isLocalhost) {
25
+ return 'http://localhost:7860';
26
+ }
27
 
28
+ // For custom deployments, use the current origin
29
+ return window.location.origin;
30
+ };
31
+
32
+ // Get WebSocket URL based on environment
33
+ const getWebSocketUrl = () => {
34
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
35
+ const host = isLocalhost ? 'localhost:7860' : window.location.host;
36
+ return `${protocol}//${host}`;
37
+ };
38
+
39
+ const API_BASE = getApiBaseUrl();
40
+ const WS_BASE = getWebSocketUrl();
41
+
42
+ return {
43
+ // API Configuration
44
+ API_BASE: API_BASE,
45
+ WS_BASE: WS_BASE,
46
+
47
+ // Environment flags
48
+ IS_HUGGINGFACE_SPACES: isHuggingFaceSpaces,
49
+ IS_LOCALHOST: isLocalhost,
50
+
51
+ // API Endpoints
52
+ ENDPOINTS: {
53
+ // Health & Status
54
+ HEALTH: `${API_BASE}/health`,
55
+ API_INFO: `${API_BASE}/api-info`,
56
+ STATUS: `${API_BASE}/api/status`,
57
+
58
+ // Provider Management
59
+ PROVIDERS: `${API_BASE}/api/providers`,
60
+ CATEGORIES: `${API_BASE}/api/categories`,
61
+
62
+ // Data Collection
63
+ PRICES: `${API_BASE}/api/prices`,
64
+ NEWS: `${API_BASE}/api/news`,
65
+ SENTIMENT: `${API_BASE}/api/sentiment/current`,
66
+ WHALES: `${API_BASE}/api/whales/transactions`,
67
+
68
+ // HuggingFace Integration
69
+ HF_HEALTH: `${API_BASE}/api/hf/health`,
70
+ HF_REGISTRY: `${API_BASE}/api/hf/registry`,
71
+ HF_SEARCH: `${API_BASE}/api/hf/search`,
72
+ HF_REFRESH: `${API_BASE}/api/hf/refresh`,
73
+ HF_RUN_SENTIMENT: `${API_BASE}/api/hf/run-sentiment`,
74
+
75
+ // Monitoring
76
+ LOGS: `${API_BASE}/api/logs`,
77
+ ALERTS: `${API_BASE}/api/alerts`,
78
+ SCHEDULER: `${API_BASE}/api/scheduler/status`,
79
+
80
+ // Analytics
81
+ ANALYTICS: `${API_BASE}/api/analytics/failures`,
82
+ RATE_LIMITS: `${API_BASE}/api/rate-limits`,
83
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ // WebSocket Endpoints
86
+ WEBSOCKETS: {
87
+ MASTER: `${WS_BASE}/ws`,
88
+ LIVE: `${WS_BASE}/ws/live`,
89
+ DATA: `${WS_BASE}/ws/data`,
90
+ MARKET_DATA: `${WS_BASE}/ws/market_data`,
91
+ NEWS: `${WS_BASE}/ws/news`,
92
+ SENTIMENT: `${WS_BASE}/ws/sentiment`,
93
+ WHALE_TRACKING: `${WS_BASE}/ws/whale_tracking`,
94
+ HEALTH: `${WS_BASE}/ws/health`,
95
+ MONITORING: `${WS_BASE}/ws/monitoring`,
96
+ HUGGINGFACE: `${WS_BASE}/ws/huggingface`,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
+ // Utility Functions
100
+ buildUrl: (path) => {
101
+ return `${API_BASE}${path}`;
102
+ },
 
 
 
 
 
 
 
 
 
 
103
 
104
+ buildWsUrl: (path) => {
105
+ return `${WS_BASE}${path}`;
106
+ },
107
 
108
+ // Fetch helper with error handling
109
+ fetchJSON: async (url, options = {}) => {
110
+ try {
111
+ const response = await fetch(url, options);
112
+ if (!response.ok) {
113
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
114
+ }
115
+ return await response.json();
116
+ } catch (error) {
117
+ console.error(`Fetch error for ${url}:`, error);
118
+ throw error;
119
  }
120
+ },
 
 
 
 
121
 
122
+ // POST helper
123
+ postJSON: async (url, body = {}) => {
124
+ return CONFIG.fetchJSON(url, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ },
129
+ body: JSON.stringify(body),
130
+ });
131
+ },
132
+ };
133
+ })();
134
 
135
+ // Export for use in modules (if needed)
136
+ if (typeof module !== 'undefined' && module.exports) {
137
+ module.exports = CONFIG;
 
 
 
138
  }
139
 
140
+ // Log configuration on load (for debugging)
141
+ console.log('🚀 Crypto API Monitor - Configuration loaded:', {
142
+ environment: CONFIG.IS_HUGGINGFACE_SPACES ? 'HuggingFace Spaces' :
143
+ CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
144
+ apiBase: CONFIG.API_BASE,
145
+ wsBase: CONFIG.WS_BASE,
146
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
hf_unified_server.py CHANGED
@@ -432,45 +432,8 @@ async def get_market_overview():
432
  raise HTTPException(status_code=503, detail="Unable to fetch market data")
433
 
434
  # Calculate market stats
435
- # Try multiple field names for market cap and volume
436
- total_market_cap = 0
437
- total_volume = 0
438
-
439
- for p in prices:
440
- # Try different field names for market cap
441
- market_cap = (
442
- p.get("market_cap") or
443
- p.get("market_cap_usd") or
444
- p.get("market_cap_rank") or # Sometimes this is the value
445
- None
446
- )
447
- # If market_cap is not found, try calculating from price and supply
448
- if not market_cap:
449
- price = p.get("price") or p.get("current_price") or 0
450
- supply = p.get("circulating_supply") or p.get("total_supply") or 0
451
- if price and supply:
452
- market_cap = float(price) * float(supply)
453
-
454
- if market_cap:
455
- try:
456
- total_market_cap += float(market_cap)
457
- except (TypeError, ValueError):
458
- pass
459
-
460
- # Try different field names for volume
461
- volume = (
462
- p.get("total_volume") or
463
- p.get("volume_24h") or
464
- p.get("volume_24h_usd") or
465
- None
466
- )
467
- if volume:
468
- try:
469
- total_volume += float(volume)
470
- except (TypeError, ValueError):
471
- pass
472
-
473
- logger.info(f"Market overview: {len(prices)} coins, total_market_cap={total_market_cap:,.0f}, total_volume={total_volume:,.0f}")
474
 
475
  # Sort by 24h change
476
  gainers = sorted(
@@ -909,14 +872,6 @@ async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
909
  # HTML Routes - Serve UI files
910
  # ============================================================================
911
 
912
- @app.get("/favicon.ico")
913
- async def favicon():
914
- """Serve favicon"""
915
- favicon_path = WORKSPACE_ROOT / "static" / "favicon.ico"
916
- if favicon_path.exists():
917
- return FileResponse(favicon_path)
918
- return JSONResponse({"status": "no favicon"}, status_code=404)
919
-
920
  @app.get("/", response_class=HTMLResponse)
921
  async def root():
922
  """Serve main admin dashboard (admin.html)"""
@@ -989,187 +944,6 @@ async def serve_html(filename: str):
989
  # Startup Event
990
  # ============================================================================
991
 
992
-
993
- # ============================================================================
994
- # ADMIN DASHBOARD ENDPOINTS
995
- # ============================================================================
996
-
997
- from fastapi import WebSocket, WebSocketDisconnect
998
- import asyncio
999
-
1000
- class ConnectionManager:
1001
- def __init__(self):
1002
- self.active_connections = []
1003
- async def connect(self, websocket: WebSocket):
1004
- await websocket.accept()
1005
- self.active_connections.append(websocket)
1006
- def disconnect(self, websocket: WebSocket):
1007
- if websocket in self.active_connections:
1008
- self.active_connections.remove(websocket)
1009
- async def broadcast(self, message: dict):
1010
- for conn in list(self.active_connections):
1011
- try:
1012
- await conn.send_json(message)
1013
- except:
1014
- self.disconnect(conn)
1015
-
1016
- ws_manager = ConnectionManager()
1017
-
1018
- @app.get("/api/health")
1019
- async def api_health():
1020
- h = await health()
1021
- return {"status": "healthy" if h.get("status") == "ok" else "degraded", **h}
1022
-
1023
- @app.get("/api/coins/top")
1024
- async def get_top_coins(limit: int = Query(default=10, ge=1, le=100)):
1025
- coins = await market_collector.get_top_coins(limit=limit)
1026
- result = [{"rank": c.get("rank", 0), "symbol": c.get("symbol", "").upper(), "name": c.get("name", ""),
1027
- "price": c.get("price") or c.get("current_price", 0),
1028
- "price_change_24h": c.get("change_24h") or c.get("price_change_percentage_24h", 0),
1029
- "volume_24h": c.get("volume_24h") or c.get("total_volume", 0), "market_cap": c.get("market_cap", 0)} for c in coins]
1030
- return {"success": True, "coins": result, "count": len(result)}
1031
-
1032
- @app.get("/api/coins/{symbol}")
1033
- async def get_coin_detail(symbol: str):
1034
- coins = await market_collector.get_top_coins(limit=250)
1035
- coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
1036
- if not coin:
1037
- raise HTTPException(404, f"Coin {symbol} not found")
1038
- return {"success": True, "symbol": symbol.upper(), "name": coin.get("name", ""),
1039
- "price": coin.get("price") or coin.get("current_price", 0),
1040
- "change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1041
- "market_cap": coin.get("market_cap", 0)}
1042
-
1043
- @app.get("/api/market/stats")
1044
- async def get_market_stats():
1045
- """Get global market statistics (duplicate endpoint - keeping for compatibility)"""
1046
- try:
1047
- overview = await get_market_overview()
1048
-
1049
- # Calculate ETH dominance from prices if available
1050
- eth_dominance = 0
1051
- if overview.get("total_market_cap", 0) > 0:
1052
- try:
1053
- eth_prices = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1054
- if eth_prices and len(eth_prices) > 0:
1055
- eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1056
- eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1057
- except:
1058
- pass
1059
-
1060
- return {
1061
- "success": True,
1062
- "stats": {
1063
- "total_market_cap": overview.get("total_market_cap", 0) or 0,
1064
- "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1065
- "btc_dominance": overview.get("btc_dominance", 0) or 0,
1066
- "eth_dominance": eth_dominance,
1067
- "active_cryptocurrencies": 10000,
1068
- "markets": 500,
1069
- "market_cap_change_24h": 0.0,
1070
- "timestamp": datetime.now().isoformat()
1071
- }
1072
- }
1073
- except Exception as e:
1074
- logger.error(f"Error in /api/market/stats (duplicate): {e}")
1075
- return {
1076
- "success": True,
1077
- "stats": {
1078
- "total_market_cap": 0,
1079
- "total_volume_24h": 0,
1080
- "btc_dominance": 0,
1081
- "eth_dominance": 0,
1082
- "active_cryptocurrencies": 0,
1083
- "markets": 0,
1084
- "market_cap_change_24h": 0.0,
1085
- "timestamp": datetime.now().isoformat()
1086
- }
1087
- }
1088
-
1089
- @app.get("/api/news/latest")
1090
- async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
1091
- from ai_models import analyze_news_item
1092
- news = await news_collector.get_latest_news(limit=limit)
1093
- enriched = []
1094
- for item in news[:limit]:
1095
- try:
1096
- e = analyze_news_item(item)
1097
- enriched.append({"title": e.get("title", ""), "source": e.get("source", ""),
1098
- "published_at": e.get("published_at") or e.get("date", ""),
1099
- "symbols": e.get("symbols", []), "sentiment": e.get("sentiment", "neutral"),
1100
- "sentiment_confidence": e.get("sentiment_confidence", 0.5)})
1101
- except:
1102
- enriched.append({"title": item.get("title", ""), "source": item.get("source", ""),
1103
- "published_at": item.get("date", ""), "symbols": item.get("symbols", []),
1104
- "sentiment": "neutral", "sentiment_confidence": 0.5})
1105
- return {"success": True, "news": enriched, "count": len(enriched)}
1106
-
1107
- @app.post("/api/news/summarize")
1108
- async def summarize_news(item: Dict[str, Any] = Body(...)):
1109
- from ai_models import analyze_news_item
1110
- e = analyze_news_item(item)
1111
- return {"success": True, "summary": e.get("title", ""), "sentiment": e.get("sentiment", "neutral")}
1112
-
1113
- # Duplicate endpoints removed - using the improved versions below in CHARTS ENDPOINTS section
1114
-
1115
- @app.post("/api/sentiment/analyze")
1116
- async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
1117
- from ai_models import ensemble_crypto_sentiment
1118
- result = ensemble_crypto_sentiment(payload.get("text", ""))
1119
- return {"success": True, "sentiment": result["label"], "confidence": result["confidence"], "details": result}
1120
-
1121
- @app.post("/api/query")
1122
- async def process_query(payload: Dict[str, Any] = Body(...)):
1123
- query = payload.get("query", "").lower()
1124
- if "price" in query or "btc" in query:
1125
- coins = await market_collector.get_top_coins(limit=10)
1126
- btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
1127
- if btc:
1128
- return {"success": True, "type": "price", "message": f"Bitcoin is ${btc.get('price', 0):,.2f}", "data": btc}
1129
- return {"success": True, "type": "general", "message": "Query processed"}
1130
-
1131
- @app.get("/api/datasets/list")
1132
- async def list_datasets():
1133
- from backend.services.hf_registry import REGISTRY
1134
- datasets = REGISTRY.list(kind="datasets")
1135
- formatted = [{"name": d.get("id"), "category": d.get("category", "other"), "tags": d.get("tags", [])} for d in datasets]
1136
- return {"success": True, "datasets": formatted, "count": len(formatted)}
1137
-
1138
- @app.get("/api/datasets/sample")
1139
- async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
1140
- return {"success": False, "name": name, "sample": [], "message": "Auth required"}
1141
-
1142
- @app.get("/api/models/list")
1143
- async def list_models():
1144
- from ai_models import get_model_info
1145
- info = get_model_info()
1146
- models = []
1147
- for cat, mlist in info.get("model_catalog", {}).items():
1148
- for mid in mlist:
1149
- models.append({"name": mid, "task": "sentiment" if "sentiment" in cat else "analysis", "category": cat})
1150
- return {"success": True, "models": models, "count": len(models)}
1151
-
1152
- @app.post("/api/models/test")
1153
- async def test_model(payload: Dict[str, Any] = Body(...)):
1154
- from ai_models import ensemble_crypto_sentiment
1155
- result = ensemble_crypto_sentiment(payload.get("text", ""))
1156
- return {"success": True, "model": payload.get("model", ""), "result": result}
1157
-
1158
- @app.websocket("/ws")
1159
- async def websocket_endpoint(websocket: WebSocket):
1160
- await ws_manager.connect(websocket)
1161
- try:
1162
- while True:
1163
- top_coins = await market_collector.get_top_coins(limit=5)
1164
- news = await news_collector.get_latest_news(limit=3)
1165
- from ai_models import ensemble_crypto_sentiment
1166
- sentiment = ensemble_crypto_sentiment(" ".join([n.get("title", "") for n in news])) if news else {"label": "neutral", "confidence": 0.5}
1167
- await websocket.send_json({"type": "update", "payload": {"market_data": top_coins, "news": news, "sentiment": sentiment, "timestamp": datetime.now().isoformat()}})
1168
- await asyncio.sleep(10)
1169
- except WebSocketDisconnect:
1170
- ws_manager.disconnect(websocket)
1171
-
1172
-
1173
  @app.on_event("startup")
1174
  async def startup_event():
1175
  """Initialize on startup"""
@@ -1337,26 +1111,14 @@ async def get_coin_detail(symbol: str):
1337
  async def get_market_stats():
1338
  """Get global market statistics"""
1339
  try:
1340
- # Use existing endpoint - get_market_overview returns total_market_cap and total_volume_24h
1341
  overview = await get_market_overview()
1342
 
1343
- # Calculate ETH dominance from prices if available
1344
- eth_dominance = 0
1345
- if overview.get("total_market_cap", 0) > 0:
1346
- # Try to get ETH market cap from top coins
1347
- try:
1348
- eth_prices = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1349
- if eth_prices and len(eth_prices) > 0:
1350
- eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1351
- eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1352
- except:
1353
- pass
1354
-
1355
  stats = {
1356
- "total_market_cap": overview.get("total_market_cap", 0) or 0,
1357
- "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1358
- "btc_dominance": overview.get("btc_dominance", 0) or 0,
1359
- "eth_dominance": eth_dominance,
1360
  "active_cryptocurrencies": 10000, # Approximate
1361
  "markets": 500, # Approximate
1362
  "market_cap_change_24h": 0.0,
@@ -1441,112 +1203,30 @@ async def summarize_news(item: Dict[str, Any] = Body(...)):
1441
  async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
1442
  """Get price chart data"""
1443
  try:
1444
- # Clean and validate symbol
1445
- symbol = symbol.strip().upper()
1446
- if not symbol:
1447
- return JSONResponse(
1448
- status_code=400,
1449
- content={
1450
- "success": False,
1451
- "symbol": "",
1452
- "timeframe": timeframe,
1453
- "data": [],
1454
- "count": 0,
1455
- "error": "Symbol cannot be empty"
1456
- }
1457
- )
1458
 
1459
- logger.info(f"Fetching price history for {symbol} with timeframe {timeframe}")
1460
-
1461
- # market_collector.get_price_history expects timeframe as string, not hours
1462
- price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
1463
-
1464
- if not price_history or len(price_history) == 0:
1465
- logger.warning(f"No price history returned for {symbol}")
1466
- return {
1467
- "success": True,
1468
- "symbol": symbol,
1469
- "timeframe": timeframe,
1470
- "data": [],
1471
- "count": 0,
1472
- "message": "No data available"
1473
- }
1474
 
1475
  chart_data = []
1476
  for point in price_history:
1477
- # Handle different timestamp formats
1478
- timestamp = point.get("timestamp") or point.get("time") or point.get("date")
1479
- price = point.get("price") or point.get("close") or point.get("value") or 0
1480
-
1481
- # Convert timestamp to ISO format if needed
1482
- if timestamp:
1483
- try:
1484
- # If it's already a string, use it
1485
- if isinstance(timestamp, str):
1486
- # Try to parse and format
1487
- try:
1488
- # Try ISO format first
1489
- dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
1490
- timestamp = dt.isoformat()
1491
- except:
1492
- try:
1493
- # Try other common formats
1494
- from dateutil import parser
1495
- dt = parser.parse(timestamp)
1496
- timestamp = dt.isoformat()
1497
- except:
1498
- pass
1499
- elif isinstance(timestamp, (int, float)):
1500
- # Unix timestamp
1501
- dt = datetime.fromtimestamp(timestamp)
1502
- timestamp = dt.isoformat()
1503
- except Exception as e:
1504
- logger.warning(f"Error parsing timestamp {timestamp}: {e}")
1505
-
1506
  chart_data.append({
1507
- "timestamp": timestamp or "",
1508
- "time": timestamp or "",
1509
- "date": timestamp or "",
1510
- "price": float(price) if price else 0,
1511
- "close": float(price) if price else 0,
1512
- "value": float(price) if price else 0
1513
  })
1514
 
1515
- logger.info(f"Returning {len(chart_data)} data points for {symbol}")
1516
-
1517
  return {
1518
  "success": True,
1519
- "symbol": symbol,
1520
  "timeframe": timeframe,
1521
  "data": chart_data,
1522
  "count": len(chart_data)
1523
  }
1524
- except CollectorError as e:
1525
- logger.error(f"Collector error in /api/charts/price/{symbol}: {e}", exc_info=True)
1526
- return JSONResponse(
1527
- status_code=200,
1528
- content={
1529
- "success": False,
1530
- "symbol": symbol.upper() if symbol else "",
1531
- "timeframe": timeframe,
1532
- "data": [],
1533
- "count": 0,
1534
- "error": str(e)
1535
- }
1536
- )
1537
  except Exception as e:
1538
- logger.error(f"Error in /api/charts/price/{symbol}: {e}", exc_info=True)
1539
- return JSONResponse(
1540
- status_code=200,
1541
- content={
1542
- "success": False,
1543
- "symbol": symbol.upper() if symbol else "",
1544
- "timeframe": timeframe,
1545
- "data": [],
1546
- "count": 0,
1547
- "error": str(e)
1548
- }
1549
- )
1550
 
1551
 
1552
  @app.post("/api/charts/analyze")
@@ -1557,38 +1237,12 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
1557
  timeframe = payload.get("timeframe", "7d")
1558
  indicators = payload.get("indicators", [])
1559
 
1560
- if not symbol:
1561
- return JSONResponse(
1562
- status_code=400,
1563
- content={"success": False, "error": "Symbol is required"}
1564
- )
1565
-
1566
- symbol = symbol.strip().upper()
1567
- logger.info(f"Analyzing chart for {symbol} with timeframe {timeframe}")
1568
-
1569
- # Get price data - use timeframe string, not hours
1570
- price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
1571
-
1572
- if not price_history or len(price_history) == 0:
1573
- return {
1574
- "success": False,
1575
- "symbol": symbol,
1576
- "timeframe": timeframe,
1577
- "error": "No price data available for analysis"
1578
- }
1579
 
1580
  # Analyze with AI
1581
  from ai_models import analyze_chart_points
1582
- try:
1583
- analysis = analyze_chart_points(price_history, indicators)
1584
- except Exception as ai_error:
1585
- logger.error(f"AI analysis error: {ai_error}", exc_info=True)
1586
- # Return a basic analysis if AI fails
1587
- analysis = {
1588
- "direction": "neutral",
1589
- "summary": "Analysis unavailable",
1590
- "signals": []
1591
- }
1592
 
1593
  return {
1594
  "success": True,
@@ -1596,18 +1250,9 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
1596
  "timeframe": timeframe,
1597
  "analysis": analysis
1598
  }
1599
- except CollectorError as e:
1600
- logger.error(f"Collector error in /api/charts/analyze: {e}", exc_info=True)
1601
- return JSONResponse(
1602
- status_code=200,
1603
- content={"success": False, "error": str(e)}
1604
- )
1605
  except Exception as e:
1606
- logger.error(f"Error in /api/charts/analyze: {e}", exc_info=True)
1607
- return JSONResponse(
1608
- status_code=200,
1609
- content={"success": False, "error": str(e)}
1610
- )
1611
 
1612
 
1613
  # ===== SENTIMENT ENDPOINTS =====
 
432
  raise HTTPException(status_code=503, detail="Unable to fetch market data")
433
 
434
  # Calculate market stats
435
+ total_market_cap = sum(p.get("market_cap", 0) or 0 for p in prices)
436
+ total_volume = sum(p.get("total_volume", 0) or 0 for p in prices)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  # Sort by 24h change
439
  gainers = sorted(
 
872
  # HTML Routes - Serve UI files
873
  # ============================================================================
874
 
 
 
 
 
 
 
 
 
875
  @app.get("/", response_class=HTMLResponse)
876
  async def root():
877
  """Serve main admin dashboard (admin.html)"""
 
944
  # Startup Event
945
  # ============================================================================
946
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  @app.on_event("startup")
948
  async def startup_event():
949
  """Initialize on startup"""
 
1111
  async def get_market_stats():
1112
  """Get global market statistics"""
1113
  try:
1114
+ # Use existing endpoint
1115
  overview = await get_market_overview()
1116
 
 
 
 
 
 
 
 
 
 
 
 
 
1117
  stats = {
1118
+ "total_market_cap": overview.get("global_market_cap", 0),
1119
+ "total_volume_24h": overview.get("global_volume", 0),
1120
+ "btc_dominance": overview.get("btc_dominance", 0),
1121
+ "eth_dominance": overview.get("eth_dominance", 0),
1122
  "active_cryptocurrencies": 10000, # Approximate
1123
  "markets": 500, # Approximate
1124
  "market_cap_change_24h": 0.0,
 
1203
  async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
1204
  """Get price chart data"""
1205
  try:
1206
+ # Map timeframe to hours
1207
+ timeframe_map = {"1d": 24, "7d": 168, "30d": 720, "90d": 2160, "1y": 8760}
1208
+ hours = timeframe_map.get(timeframe, 168)
 
 
 
 
 
 
 
 
 
 
 
1209
 
1210
+ price_history = await market_collector.get_price_history(symbol, hours=hours)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1211
 
1212
  chart_data = []
1213
  for point in price_history:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  chart_data.append({
1215
+ "timestamp": point.get("timestamp", ""),
1216
+ "price": point.get("price", 0),
1217
+ "date": point.get("timestamp", "")
 
 
 
1218
  })
1219
 
 
 
1220
  return {
1221
  "success": True,
1222
+ "symbol": symbol.upper(),
1223
  "timeframe": timeframe,
1224
  "data": chart_data,
1225
  "count": len(chart_data)
1226
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1227
  except Exception as e:
1228
+ logger.error(f"Error in /api/charts/price/{symbol}: {e}")
1229
+ raise HTTPException(status_code=503, detail=str(e))
 
 
 
 
 
 
 
 
 
 
1230
 
1231
 
1232
  @app.post("/api/charts/analyze")
 
1237
  timeframe = payload.get("timeframe", "7d")
1238
  indicators = payload.get("indicators", [])
1239
 
1240
+ # Get price data
1241
+ price_history = await market_collector.get_price_history(symbol, hours=168)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1242
 
1243
  # Analyze with AI
1244
  from ai_models import analyze_chart_points
1245
+ analysis = analyze_chart_points(price_history, indicators)
 
 
 
 
 
 
 
 
 
1246
 
1247
  return {
1248
  "success": True,
 
1250
  "timeframe": timeframe,
1251
  "analysis": analysis
1252
  }
 
 
 
 
 
 
1253
  except Exception as e:
1254
+ logger.error(f"Error in /api/charts/analyze: {e}")
1255
+ return {"success": False, "error": str(e)}
 
 
 
1256
 
1257
 
1258
  # ===== SENTIMENT ENDPOINTS =====
index.html CHANGED
@@ -1,765 +1,1216 @@
1
- <!DOCTYPE html>
2
- <html lang="en" dir="ltr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <meta name="description" content="Advanced Cryptocurrency Dashboard with AI-Powered Analytics">
7
- <title>HTS Crypto Dashboard - Professional Trading & Analysis Platform</title>
8
-
9
- <!-- Fonts - Enhanced for High Resolution -->
10
- <link rel="preconnect" href="https://fonts.googleapis.com">
11
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
13
-
14
- <!-- Icons -->
15
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
16
-
17
- <!-- Static CSS Files -->
18
- <link rel="stylesheet" href="/static/css/base.css">
19
- <link rel="stylesheet" href="/static/css/design-tokens.css">
20
- <link rel="stylesheet" href="/static/css/design-system.css">
21
- <link rel="stylesheet" href="/static/css/components.css">
22
- <link rel="stylesheet" href="/static/css/dashboard.css">
23
- <link rel="stylesheet" href="/static/css/navigation.css">
24
- <link rel="stylesheet" href="/static/css/connection-status.css">
25
- <link rel="stylesheet" href="/static/css/toast.css">
26
- <link rel="stylesheet" href="/static/css/mobile-responsive.css">
27
- <link rel="stylesheet" href="/static/css/accessibility.css">
28
- <link rel="stylesheet" href="/static/css/enterprise-components.css">
29
-
30
- <!-- Main Styles -->
31
- <link rel="stylesheet" href="styles.css">
32
- </head>
33
- <body>
34
-
35
- <!-- ============================================= -->
36
- <!-- CONNECTION STATUS BAR -->
37
- <!-- ============================================= -->
38
- <div class="connection-status-bar" id="connection-status-bar">
39
- <div class="status-left">
40
- <span class="status-dot" id="ws-status-dot"></span>
41
- <span class="status-text" id="ws-status-text">Connecting...</span>
42
- </div>
43
-
44
- <div class="status-center">
45
- <span class="system-title">HTS Crypto Monitor</span>
46
- </div>
47
-
48
- <div class="status-right">
49
- <div class="online-users-widget">
50
- <i class="fas fa-users"></i>
51
- <span id="active-users-count">0</span>
52
- <span class="label-small">Online Users</span>
53
- </div>
54
- </div>
55
- </div>
56
-
57
- <!-- ============================================= -->
58
- <!-- MAIN HEADER -->
59
- <!-- ============================================= -->
60
- <header class="main-header">
61
- <div class="header-container">
62
- <div class="header-left">
63
- <div class="logo-section">
64
- <i class="fas fa-chart-line logo-icon"></i>
65
- <h1 class="app-title">HTS Dashboard</h1>
66
- </div>
67
- </div>
68
-
69
- <div class="header-center">
70
- <div class="search-box">
71
- <i class="fas fa-search"></i>
72
- <input type="text" placeholder="Search coins, news, analysis..." id="global-search">
73
- </div>
74
- </div>
75
-
76
- <div class="header-right">
77
- <button class="icon-btn" id="theme-toggle" title="Toggle Theme">
78
- <i class="fas fa-moon"></i>
79
- </button>
80
-
81
- <button class="icon-btn" id="notifications-btn" title="Notifications">
82
- <i class="fas fa-bell"></i>
83
- <span class="notification-badge" id="notification-count">0</span>
84
- </button>
85
-
86
- <button class="icon-btn" id="settings-btn" title="Settings">
87
- <i class="fas fa-cog"></i>
88
- </button>
89
- </div>
90
- </div>
91
- </header>
92
-
93
- <!-- ============================================= -->
94
- <!-- DESKTOP NAVIGATION -->
95
- <!-- ============================================= -->
96
- <nav class="desktop-nav">
97
- <ul class="nav-tabs">
98
- <li class="nav-tab">
99
- <button class="nav-tab-btn active" data-view="overview">
100
- <span class="nav-tab-icon"><i class="fas fa-home"></i></span>
101
- <span class="nav-tab-label">Overview</span>
102
- </button>
103
- </li>
104
- <li class="nav-tab">
105
- <button class="nav-tab-btn" data-view="market">
106
- <span class="nav-tab-icon"><i class="fas fa-chart-bar"></i></span>
107
- <span class="nav-tab-label">Market</span>
108
- </button>
109
- </li>
110
- <li class="nav-tab">
111
- <button class="nav-tab-btn" data-view="charts">
112
- <span class="nav-tab-icon"><i class="fas fa-chart-area"></i></span>
113
- <span class="nav-tab-label">Charts</span>
114
- </button>
115
- </li>
116
- <li class="nav-tab">
117
- <button class="nav-tab-btn" data-view="news">
118
- <span class="nav-tab-icon"><i class="fas fa-newspaper"></i></span>
119
- <span class="nav-tab-label">News</span>
120
- </button>
121
- </li>
122
- <li class="nav-tab">
123
- <button class="nav-tab-btn" data-view="ai">
124
- <span class="nav-tab-icon"><i class="fas fa-robot"></i></span>
125
- <span class="nav-tab-label">AI Analysis</span>
126
- </button>
127
- </li>
128
- <li class="nav-tab">
129
- <button class="nav-tab-btn" data-view="providers">
130
- <span class="nav-tab-icon"><i class="fas fa-plug"></i></span>
131
- <span class="nav-tab-label">Providers</span>
132
- </button>
133
- </li>
134
- <li class="nav-tab">
135
- <button class="nav-tab-btn" data-view="api-explorer">
136
- <span class="nav-tab-icon"><i class="fas fa-code"></i></span>
137
- <span class="nav-tab-label">API Explorer</span>
138
- </button>
139
- </li>
140
- </ul>
141
- </nav>
142
-
143
- <!-- ============================================= -->
144
- <!-- MOBILE NAVIGATION -->
145
- <!-- ============================================= -->
146
- <nav class="mobile-nav">
147
- <ul class="mobile-nav-tabs">
148
- <li class="mobile-nav-tab">
149
- <button class="mobile-nav-tab-btn active" data-view="overview">
150
- <span class="mobile-nav-tab-icon"><i class="fas fa-home"></i></span>
151
- <span class="mobile-nav-tab-label">Home</span>
152
- </button>
153
- </li>
154
- <li class="mobile-nav-tab">
155
- <button class="mobile-nav-tab-btn" data-view="market">
156
- <span class="mobile-nav-tab-icon"><i class="fas fa-chart-bar"></i></span>
157
- <span class="mobile-nav-tab-label">Market</span>
158
- </button>
159
- </li>
160
- <li class="mobile-nav-tab">
161
- <button class="mobile-nav-tab-btn" data-view="charts">
162
- <span class="mobile-nav-tab-icon"><i class="fas fa-chart-area"></i></span>
163
- <span class="mobile-nav-tab-label">Charts</span>
164
- </button>
165
- </li>
166
- <li class="mobile-nav-tab">
167
- <button class="mobile-nav-tab-btn" data-view="news">
168
- <span class="mobile-nav-tab-icon"><i class="fas fa-newspaper"></i></span>
169
- <span class="mobile-nav-tab-label">News</span>
170
- </button>
171
- </li>
172
- <li class="mobile-nav-tab">
173
- <button class="mobile-nav-tab-btn" data-view="ai">
174
- <span class="mobile-nav-tab-icon"><i class="fas fa-robot"></i></span>
175
- <span class="mobile-nav-tab-label">AI</span>
176
- </button>
177
- </li>
178
- </ul>
179
- </nav>
180
-
181
- <!-- ============================================= -->
182
- <!-- MAIN CONTENT -->
183
- <!-- ============================================= -->
184
- <main class="dashboard-main">
185
-
186
- <!-- OVERVIEW SECTION -->
187
- <section class="view-section active" id="view-overview">
188
- <!-- Market Overview - New Layout: 3 Main Metrics + 12 Coin Cards -->
189
- <div class="market-overview-layout">
190
- <!-- Left Column: 3 Main Market Metrics -->
191
- <div class="main-metrics-column">
192
- <div class="main-metric-card">
193
- <div class="main-metric-header">
194
- <div class="main-metric-icon">
195
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
196
- <line x1="12" y1="1" x2="12" y2="23" stroke="white" stroke-width="2.5"></line>
197
- <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke="white" stroke-width="2.5" fill="rgba(255,255,255,0.1)"></path>
198
- </svg>
199
- </div>
200
- <span class="main-metric-label">Total Market Cap</span>
201
- </div>
202
- <div class="main-metric-value" id="total-market-cap">$2.5T</div>
203
- <div class="main-metric-change positive" id="market-cap-change">
204
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
205
- <polyline points="18 15 12 9 6 15"></polyline>
206
- </svg>
207
- <span>+2.4%</span>
208
- </div>
209
- </div>
210
-
211
- <div class="main-metric-card">
212
- <div class="main-metric-header">
213
- <div class="main-metric-icon">
214
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
215
- <circle cx="12" cy="12" r="10" stroke="white" stroke-width="2.5" fill="rgba(255,255,255,0.1)"></circle>
216
- <path d="M12 6v6l4 2" stroke="white" stroke-width="2.5" stroke-linecap="round"></path>
217
- </svg>
218
- </div>
219
- <span class="main-metric-label">24h Volume</span>
220
- </div>
221
- <div class="main-metric-value" id="volume-24h">$125B</div>
222
- <div class="main-metric-change negative" id="volume-change">
223
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
224
- <polyline points="6 9 12 15 18 9"></polyline>
225
- </svg>
226
- <span>-1.2%</span>
227
- </div>
228
- </div>
229
-
230
- <div class="main-metric-card">
231
- <div class="main-metric-header">
232
- <div class="main-metric-icon">
233
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
234
- <line x1="12" y1="2" x2="12" y2="22" stroke="white" stroke-width="2.5"></line>
235
- <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke="white" stroke-width="2.5" fill="rgba(255,255,255,0.1)"></path>
236
- <polyline points="9 12 12 9 15 12" stroke="white" stroke-width="2.5" fill="none"></polyline>
237
- </svg>
238
- </div>
239
- <span class="main-metric-label">Market Trend</span>
240
- </div>
241
- <div class="main-metric-value" id="market-trend">Bullish</div>
242
- <div class="main-metric-change positive" id="trend-change">
243
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
244
- <polyline points="18 15 12 9 6 15"></polyline>
245
- </svg>
246
- <span>+5.6%</span>
247
- </div>
248
- </div>
249
- </div>
250
-
251
- <!-- Right Column: 12 Coin Cards Grid -->
252
- <div class="coins-grid-compact" id="coins-grid-compact">
253
- <!-- Coin cards will be inserted here dynamically -->
254
- </div>
255
- </div>
256
-
257
- <!-- Additional Stats (Hidden for now, can be shown if needed) -->
258
- <div class="stats-grid-compact" style="display: none;">
259
- <div class="stat-card-compact">
260
- <div class="stat-header-compact">
261
- <span class="stat-icon-compact"><i class="fas fa-dollar-sign"></i></span>
262
- <span class="stat-label-compact">Market Cap</span>
263
- </div>
264
- <div class="stat-value-compact" id="total-market-cap">$2.5T</div>
265
- <div class="stat-change-compact positive" id="market-cap-change">+2.4%</div>
266
- </div>
267
-
268
- <div class="stat-card-compact">
269
- <div class="stat-header-compact">
270
- <span class="stat-icon-compact"><i class="fab fa-bitcoin"></i></span>
271
- <span class="stat-label-compact">BTC Dominance</span>
272
- </div>
273
- <div class="stat-value-compact" id="btc-dominance">52.3%</div>
274
- <div class="stat-change-compact positive" id="btc-dominance-change">+0.8%</div>
275
- </div>
276
-
277
- <div class="stat-card-compact">
278
- <div class="stat-header-compact">
279
- <span class="stat-icon-compact"><i class="fab fa-ethereum"></i></span>
280
- <span class="stat-label-compact">ETH Dominance</span>
281
- </div>
282
- <div class="stat-value-compact" id="eth-dominance">18.5%</div>
283
- <div class="stat-change-compact negative" id="eth-dominance-change">-0.3%</div>
284
- </div>
285
-
286
- <div class="stat-card-compact">
287
- <div class="stat-header-compact">
288
- <span class="stat-icon-compact"><i class="fas fa-chart-line"></i></span>
289
- <span class="stat-label-compact">24h Volume</span>
290
- </div>
291
- <div class="stat-value-compact" id="volume-24h">$125B</div>
292
- <div class="stat-change-compact negative" id="volume-change">-1.2%</div>
293
- </div>
294
-
295
- <div class="stat-card-compact">
296
- <div class="stat-header-compact">
297
- <span class="stat-icon-compact"><i class="fas fa-fire"></i></span>
298
- <span class="stat-label-compact">Market Trend</span>
299
- </div>
300
- <div class="stat-value-compact" id="market-trend">Bullish</div>
301
- <div class="stat-change-compact positive" id="trend-change">+5.6%</div>
302
- </div>
303
-
304
- <div class="stat-card-compact">
305
- <div class="stat-header-compact">
306
- <span class="stat-icon-compact"><i class="fas fa-coins"></i></span>
307
- <span class="stat-label-compact">Active Coins</span>
308
- </div>
309
- <div class="stat-value-compact" id="active-cryptocurrencies">10,523</div>
310
- <div class="stat-change-compact neutral" id="active-change">+127</div>
311
- </div>
312
-
313
- <div class="stat-card-compact">
314
- <div class="stat-header-compact">
315
- <span class="stat-icon-compact"><i class="fas fa-exchange-alt"></i></span>
316
- <span class="stat-label-compact">Markets</span>
317
- </div>
318
- <div class="stat-value-compact" id="markets-count">847</div>
319
- <div class="stat-change-compact neutral" id="markets-change">+12</div>
320
- </div>
321
-
322
- <div class="stat-card-compact">
323
- <div class="stat-header-compact">
324
- <span class="stat-icon-compact"><i class="fas fa-brain"></i></span>
325
- <span class="stat-label-compact">Fear & Greed</span>
326
- </div>
327
- <div class="stat-value-compact" id="fear-greed-index">65</div>
328
- <div class="stat-change-compact positive" id="fear-greed-change">Greed</div>
329
- </div>
330
-
331
- <div class="stat-card-compact">
332
- <div class="stat-header-compact">
333
- <span class="stat-icon-compact"><i class="fas fa-arrow-up"></i></span>
334
- <span class="stat-label-compact">24h Change</span>
335
- </div>
336
- <div class="stat-value-compact" id="market-cap-change-24h">+2.4%</div>
337
- <div class="stat-change-compact positive" id="market-change-24h">$58.2B</div>
338
- </div>
339
-
340
- <div class="stat-card-compact">
341
- <div class="stat-header-compact">
342
- <span class="stat-icon-compact"><i class="fas fa-chart-pie"></i></span>
343
- <span class="stat-label-compact">Top 10 Share</span>
344
- </div>
345
- <div class="stat-value-compact" id="top10-share">78.5%</div>
346
- <div class="stat-change-compact neutral" id="top10-change">-0.2%</div>
347
- </div>
348
-
349
- <div class="stat-card-compact">
350
- <div class="stat-header-compact">
351
- <span class="stat-icon-compact"><i class="fas fa-trophy"></i></span>
352
- <span class="stat-label-compact">BTC Price</span>
353
- </div>
354
- <div class="stat-value-compact" id="btc-price">$43,250</div>
355
- <div class="stat-change-compact positive" id="btc-price-change">+1.8%</div>
356
- </div>
357
-
358
- <div class="stat-card-compact">
359
- <div class="stat-header-compact">
360
- <span class="stat-icon-compact"><i class="fas fa-gem"></i></span>
361
- <span class="stat-label-compact">ETH Price</span>
362
- </div>
363
- <div class="stat-value-compact" id="eth-price">$2,650</div>
364
- <div class="stat-change-compact positive" id="eth-price-change">+2.1%</div>
365
- </div>
366
- </div>
367
-
368
- <!-- Sentiment Analysis -->
369
- <div class="sentiment-section">
370
- <div class="section-header">
371
- <h2>Market Sentiment</h2>
372
- <span class="sentiment-badge">
373
- <i class="fas fa-brain"></i>
374
- AI Powered
375
- </span>
376
- </div>
377
-
378
- <div class="sentiment-cards">
379
- <div class="sentiment-item bullish">
380
- <div class="sentiment-item-header">
381
- <div class="sentiment-icon">
382
- <i class="fas fa-arrow-up"></i>
383
- </div>
384
- <span class="sentiment-label">Bullish</span>
385
- <span class="sentiment-percent" id="bullish-percent">45%</span>
386
- </div>
387
- <div class="sentiment-progress">
388
- <div class="sentiment-progress-bar bullish" style="width: 45%"></div>
389
- </div>
390
- </div>
391
-
392
- <div class="sentiment-item neutral">
393
- <div class="sentiment-item-header">
394
- <div class="sentiment-icon">
395
- <i class="fas fa-minus"></i>
396
- </div>
397
- <span class="sentiment-label">Neutral</span>
398
- <span class="sentiment-percent" id="neutral-percent">30%</span>
399
- </div>
400
- <div class="sentiment-progress">
401
- <div class="sentiment-progress-bar neutral" style="width: 30%"></div>
402
- </div>
403
- </div>
404
-
405
- <div class="sentiment-item bearish">
406
- <div class="sentiment-item-header">
407
- <div class="sentiment-icon">
408
- <i class="fas fa-arrow-down"></i>
409
- </div>
410
- <span class="sentiment-label">Bearish</span>
411
- <span class="sentiment-percent" id="bearish-percent">25%</span>
412
- </div>
413
- <div class="sentiment-progress">
414
- <div class="sentiment-progress-bar bearish" style="width: 25%"></div>
415
- </div>
416
- </div>
417
- </div>
418
- </div>
419
-
420
- <!-- Top Coins Table -->
421
- <div class="table-section">
422
- <div class="section-header">
423
- <h2>Top Cryptocurrencies</h2>
424
- <button class="btn-secondary" id="refresh-coins">
425
- <i class="fas fa-sync"></i>
426
- Refresh
427
- </button>
428
- </div>
429
-
430
- <div class="table-container">
431
- <table class="data-table">
432
- <thead>
433
- <tr>
434
- <th>Rank</th>
435
- <th>Name</th>
436
- <th>Price</th>
437
- <th>24h Change</th>
438
- <th>Volume</th>
439
- <th>Market Cap</th>
440
- <th>Actions</th>
441
- </tr>
442
- </thead>
443
- <tbody id="coins-table-body">
444
- <tr>
445
- <td colspan="7" class="loading-cell">
446
- <div class="loader"></div>
447
- Loading...
448
- </td>
449
- </tr>
450
- </tbody>
451
- </table>
452
- </div>
453
- </div>
454
- </section>
455
-
456
- <!-- MARKET SECTION -->
457
- <section class="view-section" id="view-market">
458
- <div class="section-header">
459
- <h2>Cryptocurrency Market</h2>
460
- <div class="filter-group">
461
- <select class="filter-select" id="market-filter">
462
- <option value="all">All</option>
463
- <option value="gainers">Top Gainers</option>
464
- <option value="losers">Top Losers</option>
465
- <option value="trending">Trending</option>
466
- </select>
467
- <select class="filter-select" id="market-sort">
468
- <option value="market_cap">Market Cap</option>
469
- <option value="volume">Volume</option>
470
- <option value="price">Price</option>
471
- <option value="change">24h Change</option>
472
- </select>
473
- </div>
474
- </div>
475
-
476
- <div class="market-grid" id="market-grid">
477
- <!-- Market cards will be inserted here -->
478
- </div>
479
- </section>
480
-
481
- <!-- CHARTS SECTION -->
482
- <section class="view-section" id="view-charts">
483
- <div class="section-header">
484
- <h2>Advanced Charts</h2>
485
- <div class="chart-controls">
486
- <select class="filter-select" id="chart-symbol">
487
- <option value="BTCUSDT">BTC/USDT</option>
488
- <option value="ETHUSDT">ETH/USDT</option>
489
- <option value="BNBUSDT">BNB/USDT</option>
490
- <option value="ADAUSDT">ADA/USDT</option>
491
- <option value="SOLUSDT">SOL/USDT</option>
492
- </select>
493
- <select class="filter-select" id="chart-interval">
494
- <option value="1m">1 Minute</option>
495
- <option value="5m">5 Minutes</option>
496
- <option value="15m">15 Minutes</option>
497
- <option value="1h" selected>1 Hour</option>
498
- <option value="4h">4 Hours</option>
499
- <option value="1d">1 Day</option>
500
- </select>
501
- </div>
502
- </div>
503
-
504
- <div class="chart-container">
505
- <div id="tradingview-chart" class="tradingview-widget"></div>
506
- </div>
507
-
508
- <div class="indicators-panel">
509
- <h3>Active Indicators</h3>
510
- <div class="indicators-grid" id="indicators-grid">
511
- <!-- Indicators will be inserted here -->
512
- </div>
513
- </div>
514
- </section>
515
-
516
- <!-- NEWS SECTION -->
517
- <section class="view-section" id="view-news">
518
- <div class="section-header">
519
- <h2>Latest News & Analysis</h2>
520
- <div class="filter-group">
521
- <input type="text" class="filter-input" id="news-search" placeholder="Search news...">
522
- <select class="filter-select" id="news-filter">
523
- <option value="all">All News</option>
524
- <option value="bitcoin">Bitcoin</option>
525
- <option value="ethereum">Ethereum</option>
526
- <option value="defi">DeFi</option>
527
- <option value="nft">NFT</option>
528
- <option value="regulation">Regulation</option>
529
- </select>
530
- </div>
531
- </div>
532
-
533
- <div class="news-grid" id="news-grid">
534
- <!-- News cards will be inserted here -->
535
- </div>
536
- </section>
537
-
538
- <!-- AI SECTION -->
539
- <section class="view-section" id="view-ai">
540
- <div class="ai-header">
541
- <h2>AI-Powered Analysis</h2>
542
- <p>Advanced analytics powered by Hugging Face models</p>
543
- </div>
544
-
545
- <div class="ai-tools-grid">
546
- <div class="ai-tool-card">
547
- <div class="ai-tool-icon">
548
- <i class="fas fa-comment-dots"></i>
549
- </div>
550
- <h3>Sentiment Analysis</h3>
551
- <p>Analyze market sentiment from news and social media</p>
552
- <button class="btn-primary" id="sentiment-analysis-btn">
553
- <i class="fas fa-play"></i>
554
- Run Analysis
555
- </button>
556
- </div>
557
-
558
- <div class="ai-tool-card">
559
- <div class="ai-tool-icon">
560
- <i class="fas fa-file-alt"></i>
561
- </div>
562
- <h3>News Summarization</h3>
563
- <p>Automatically summarize long news articles with AI</p>
564
- <button class="btn-primary" id="news-summary-btn">
565
- <i class="fas fa-play"></i>
566
- Summarize
567
- </button>
568
- </div>
569
-
570
- <div class="ai-tool-card">
571
- <div class="ai-tool-icon">
572
- <i class="fas fa-chart-line"></i>
573
- </div>
574
- <h3>Price Prediction</h3>
575
- <p>Predict price trends using ML models</p>
576
- <button class="btn-primary" id="price-prediction-btn">
577
- <i class="fas fa-play"></i>
578
- Predict
579
- </button>
580
- </div>
581
-
582
- <div class="ai-tool-card">
583
- <div class="ai-tool-icon">
584
- <i class="fas fa-brain"></i>
585
- </div>
586
- <h3>Pattern Detection</h3>
587
- <p>Detect candlestick patterns and technical analysis</p>
588
- <button class="btn-primary" id="pattern-detection-btn">
589
- <i class="fas fa-play"></i>
590
- Detect Patterns
591
- </button>
592
- </div>
593
- </div>
594
-
595
- <div class="ai-results" id="ai-results" style="display: none;">
596
- <div class="section-header">
597
- <h3>Analysis Results</h3>
598
- <button class="btn-ghost" id="clear-results">
599
- <i class="fas fa-times"></i>
600
- </button>
601
- </div>
602
- <div class="ai-results-content" id="ai-results-content">
603
- <!-- AI results will be shown here -->
604
- </div>
605
- </div>
606
- </section>
607
-
608
- <!-- PROVIDERS SECTION -->
609
- <section class="view-section" id="view-providers">
610
- <div class="section-header">
611
- <h2>Data Providers</h2>
612
- <button class="btn-secondary" id="refresh-providers">
613
- <i class="fas fa-sync"></i>
614
- Refresh
615
- </button>
616
- </div>
617
-
618
- <div class="providers-grid" id="providers-grid">
619
- <!-- Provider cards will be inserted here -->
620
- </div>
621
- </section>
622
-
623
- <!-- API EXPLORER SECTION -->
624
- <section class="view-section" id="view-api-explorer">
625
- <div class="section-header">
626
- <h2>API Explorer</h2>
627
- <button class="btn-secondary" id="api-docs-link">
628
- <i class="fas fa-book"></i>
629
- View Docs
630
- </button>
631
- </div>
632
-
633
- <div class="api-explorer-container">
634
- <div class="api-endpoints-list" id="api-endpoints-list">
635
- <!-- API endpoints will be listed here -->
636
- </div>
637
- <div class="api-response-panel" id="api-response-panel">
638
- <h3>Response</h3>
639
- <pre id="api-response-content">Select an endpoint to test...</pre>
640
- </div>
641
- </div>
642
- </section>
643
-
644
- </main>
645
-
646
- <!-- ============================================= -->
647
- <!-- FLOATING STATS CARD -->
648
- <!-- ============================================= -->
649
- <div class="floating-stats-card" id="floating-stats">
650
- <div class="stats-card-header">
651
- <h3>System Stats</h3>
652
- <button class="minimize-btn" id="minimize-stats">
653
- <i class="fas fa-minus"></i>
654
- </button>
655
- </div>
656
- <div class="stats-card-body">
657
- <div class="stats-mini-grid">
658
- <div class="stat-mini">
659
- <div class="stat-mini-label">API Status</div>
660
- <div class="stat-mini-value" id="api-status">
661
- <span class="status-dot active"></span>
662
- Online
663
- </div>
664
- </div>
665
- <div class="stat-mini">
666
- <div class="stat-mini-label">Ping</div>
667
- <div class="stat-mini-value" id="ping-value">--</div>
668
- </div>
669
- <div class="stat-mini">
670
- <div class="stat-mini-label">Last Update</div>
671
- <div class="stat-mini-value" id="last-update">--</div>
672
- </div>
673
- </div>
674
- </div>
675
- </div>
676
-
677
- <!-- ============================================= -->
678
- <!-- NOTIFICATIONS PANEL -->
679
- <!-- ============================================= -->
680
- <div class="notifications-panel" id="notifications-panel">
681
- <div class="notifications-header">
682
- <h3>Notifications</h3>
683
- <button class="btn-ghost" id="close-notifications">
684
- <i class="fas fa-times"></i>
685
- </button>
686
- </div>
687
- <div class="notifications-body" id="notifications-body">
688
- <!-- Notifications will be inserted here -->
689
- </div>
690
- </div>
691
-
692
- <!-- ============================================= -->
693
- <!-- SETTINGS MODAL -->
694
- <!-- ============================================= -->
695
- <div class="settings-modal" id="settings-modal">
696
- <div class="modal-content">
697
- <div class="modal-header">
698
- <h3>Settings</h3>
699
- <button class="btn-ghost" id="close-settings">
700
- <i class="fas fa-times"></i>
701
- </button>
702
- </div>
703
- <div class="modal-body">
704
- <div class="settings-section">
705
- <h4>Display</h4>
706
- <label>
707
- <input type="checkbox" id="enable-animations" checked>
708
- Enable Animations
709
- </label>
710
- <label>
711
- <input type="checkbox" id="enable-sounds">
712
- Enable Sounds
713
- </label>
714
- </div>
715
- <div class="settings-section">
716
- <h4>Updates</h4>
717
- <label>
718
- Update Interval (seconds)
719
- <input type="number" id="update-interval" value="30" min="10" max="300">
720
- </label>
721
- </div>
722
- </div>
723
- </div>
724
- </div>
725
-
726
- <!-- ============================================= -->
727
- <!-- LOADING OVERLAY -->
728
- <!-- ============================================= -->
729
- <div class="loading-overlay" id="loading-overlay">
730
- <div class="loading-spinner"></div>
731
- <div class="loading-text">Loading...</div>
732
- </div>
733
-
734
- <!-- ============================================= -->
735
- <!-- SCRIPTS -->
736
- <!-- ============================================= -->
737
- <!-- Configuration -->
738
- <script src="config.js"></script>
739
-
740
- <!-- Static JavaScript Files -->
741
- <script src="/static/js/api-client.js"></script>
742
- <script src="/static/js/websocket-client.js"></script>
743
- <script src="/static/js/theme-manager.js"></script>
744
- <script src="/static/js/tabs.js"></script>
745
- <script src="/static/js/toast.js"></script>
746
- <script src="/static/js/accessibility.js"></script>
747
- <script src="/static/js/uiUtils.js"></script>
748
- <script src="/static/js/errorHelper.js"></script>
749
-
750
- <!-- View Controllers -->
751
- <script src="/static/js/overviewView.js"></script>
752
- <script src="/static/js/marketView.js"></script>
753
- <script src="/static/js/newsView.js"></script>
754
- <script src="/static/js/aiAdvisorView.js"></script>
755
- <script src="/static/js/providersView.js"></script>
756
- <script src="/static/js/apiExplorerView.js"></script>
757
- <script src="/static/js/chartLabView.js"></script>
758
-
759
- <!-- Main Application -->
760
- <script src="/static/js/dashboard.js"></script>
761
-
762
- <!-- Main App Initialization -->
763
- <script src="app.js" type="module"></script>
764
- </body>
765
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Crypto API Monitor</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ <style>
10
+ * { box-sizing: border-box; }
11
+ :root {
12
+ --bg: #f5f6fb;
13
+ --card: #ffffff;
14
+ --muted: #6b7280;
15
+ --text: #1f2937;
16
+ --primary: #2563eb;
17
+ --success: #16a34a;
18
+ --danger: #dc2626;
19
+ --warning: #d97706;
20
+ --border: #e5e7eb;
21
+ }
22
+ body {
23
+ margin: 0;
24
+ font-family: 'Inter', sans-serif;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ }
28
+ .container {
29
+ max-width: 1400px;
30
+ margin: 0 auto;
31
+ padding: 20px;
32
+ }
33
+ .header {
34
+ background: var(--card);
35
+ padding: 18px 24px;
36
+ border-radius: 10px;
37
+ box-shadow: 0 1px 3px rgba(15,23,42,0.08);
38
+ display: flex;
39
+ justify-content: space-between;
40
+ align-items: center;
41
+ flex-wrap: wrap;
42
+ gap: 16px;
43
+ }
44
+ .logo h1 { margin: 0; font-size: 20px; color: var(--primary); }
45
+ .header-actions { display: flex; gap: 12px; align-items: center; }
46
+ .status-pill {
47
+ padding: 6px 12px;
48
+ border-radius: 999px;
49
+ font-size: 12px;
50
+ font-weight: 600;
51
+ background: #fee2e2;
52
+ color: var(--danger);
53
+ }
54
+ .btn {
55
+ padding: 10px 18px;
56
+ border-radius: 8px;
57
+ border: none;
58
+ font-weight: 600;
59
+ font-size: 13px;
60
+ cursor: pointer;
61
+ background: var(--primary);
62
+ color: #fff;
63
+ transition: all 0.2s ease;
64
+ display: inline-flex;
65
+ align-items: center;
66
+ gap: 6px;
67
+ box-shadow: 0 2px 4px rgba(37,99,235,0.2);
68
+ }
69
+ .btn:hover {
70
+ background: #1d4ed8;
71
+ transform: translateY(-1px);
72
+ box-shadow: 0 4px 8px rgba(37,99,235,0.3);
73
+ }
74
+ .btn:active {
75
+ transform: translateY(0);
76
+ box-shadow: 0 1px 2px rgba(37,99,235,0.2);
77
+ }
78
+ .btn.secondary {
79
+ background: var(--card);
80
+ border: 1px solid var(--border);
81
+ color: var(--text);
82
+ box-shadow: 0 1px 2px rgba(15,23,42,0.1);
83
+ }
84
+ .btn.secondary:hover {
85
+ background: #f9fafb;
86
+ border-color: var(--primary);
87
+ color: var(--primary);
88
+ box-shadow: 0 2px 4px rgba(37,99,235,0.15);
89
+ }
90
+ .kpi-grid {
91
+ display: grid;
92
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
93
+ gap: 10px;
94
+ margin: 16px 0;
95
+ }
96
+ .kpi-card {
97
+ background: var(--card);
98
+ padding: 14px;
99
+ border-radius: 8px;
100
+ box-shadow: 0 1px 3px rgba(15,23,42,0.08);
101
+ border: 1px solid var(--border);
102
+ transition: all 0.2s ease;
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 8px;
106
+ }
107
+ .kpi-card:hover {
108
+ box-shadow: 0 4px 12px rgba(37,99,235,0.15);
109
+ transform: translateY(-2px);
110
+ border-color: var(--primary);
111
+ }
112
+ .kpi-header {
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: space-between;
116
+ gap: 8px;
117
+ }
118
+ .kpi-icon {
119
+ width: 32px;
120
+ height: 32px;
121
+ padding: 6px;
122
+ border-radius: 6px;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex-shrink: 0;
127
+ }
128
+ .kpi-icon svg {
129
+ width: 20px;
130
+ height: 20px;
131
+ stroke-width: 2;
132
+ }
133
+ .kpi-icon.blue { background: #dbeafe; color: #2563eb; }
134
+ .kpi-icon.green { background: #dcfce7; color: #16a34a; }
135
+ .kpi-icon.orange { background: #fed7aa; color: #ea580c; }
136
+ .kpi-icon.purple { background: #e9d5ff; color: #9333ea; }
137
+ .kpi-label {
138
+ font-size: 11px;
139
+ color: var(--muted);
140
+ letter-spacing: 0.3px;
141
+ text-transform: uppercase;
142
+ font-weight: 600;
143
+ flex: 1;
144
+ }
145
+ .kpi-value {
146
+ font-size: 24px;
147
+ font-weight: 700;
148
+ color: var(--text);
149
+ line-height: 1.2;
150
+ }
151
+ .kpi-trend {
152
+ font-size: 11px;
153
+ color: var(--muted);
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 4px;
157
+ }
158
+ .tabs {
159
+ background: var(--card);
160
+ border-radius: 10px;
161
+ padding: 8px;
162
+ display: flex;
163
+ gap: 6px;
164
+ overflow-x: auto;
165
+ box-shadow: 0 1px 3px rgba(15,23,42,0.08);
166
+ }
167
+ .tab {
168
+ border: none;
169
+ background: transparent;
170
+ padding: 8px 16px;
171
+ border-radius: 6px;
172
+ font-weight: 600;
173
+ color: var(--muted);
174
+ cursor: pointer;
175
+ }
176
+ .tab.active { background: var(--primary); color: #fff; }
177
+ .tab-content { margin-top: 16px; display: none; }
178
+ .tab-content.active { display: block; }
179
+ .card {
180
+ background: var(--card);
181
+ border-radius: 12px;
182
+ padding: 20px;
183
+ box-shadow: 0 1px 3px rgba(15,23,42,0.08);
184
+ border: 1px solid var(--border);
185
+ margin-bottom: 16px;
186
+ transition: all 0.2s ease;
187
+ }
188
+ .card:hover {
189
+ box-shadow: 0 4px 12px rgba(15,23,42,0.12);
190
+ border-color: rgba(37,99,235,0.2);
191
+ }
192
+ .card-header {
193
+ display: flex;
194
+ justify-content: space-between;
195
+ align-items: center;
196
+ margin-bottom: 16px;
197
+ padding-bottom: 12px;
198
+ border-bottom: 2px solid var(--border);
199
+ }
200
+ .card-title {
201
+ font-size: 16px;
202
+ font-weight: 700;
203
+ color: var(--text);
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 8px;
207
+ }
208
+ .grid { display: grid; gap: 16px; }
209
+ .grid-2 { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
210
+ .table-wrapper {
211
+ overflow-x: auto;
212
+ border-radius: 8px;
213
+ border: 1px solid var(--border);
214
+ }
215
+ table {
216
+ width: 100%;
217
+ border-collapse: separate;
218
+ border-spacing: 0;
219
+ font-size: 13px;
220
+ }
221
+ thead {
222
+ background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
223
+ position: sticky;
224
+ top: 0;
225
+ z-index: 10;
226
+ }
227
+ thead th {
228
+ padding: 14px 16px;
229
+ text-align: left;
230
+ font-weight: 700;
231
+ font-size: 11px;
232
+ text-transform: uppercase;
233
+ letter-spacing: 0.5px;
234
+ color: var(--muted);
235
+ border-bottom: 2px solid var(--border);
236
+ white-space: nowrap;
237
+ }
238
+ thead th:first-child { border-top-left-radius: 8px; }
239
+ thead th:last-child { border-top-right-radius: 8px; }
240
+ tbody tr {
241
+ transition: all 0.15s ease;
242
+ border-bottom: 1px solid #f3f4f6;
243
+ }
244
+ tbody tr:hover {
245
+ background: #f9fafb;
246
+ transform: scale(1.001);
247
+ box-shadow: 0 2px 8px rgba(37,99,235,0.08);
248
+ }
249
+ tbody tr:last-child td:first-child { border-bottom-left-radius: 8px; }
250
+ tbody tr:last-child td:last-child { border-bottom-right-radius: 8px; }
251
+ tbody td {
252
+ padding: 14px 16px;
253
+ color: var(--text);
254
+ vertical-align: middle;
255
+ }
256
+ tbody td strong {
257
+ font-weight: 600;
258
+ color: var(--text);
259
+ }
260
+ tbody td:first-child {
261
+ font-weight: 600;
262
+ }
263
+ .badge {
264
+ padding: 5px 12px;
265
+ border-radius: 6px;
266
+ font-size: 11px;
267
+ font-weight: 700;
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: 5px;
271
+ text-transform: uppercase;
272
+ letter-spacing: 0.3px;
273
+ border: 1px solid transparent;
274
+ transition: all 0.2s ease;
275
+ }
276
+ .badge.success {
277
+ background: #dcfce7;
278
+ color: #15803d;
279
+ border-color: #86efac;
280
+ }
281
+ .badge.warn {
282
+ background: #fef3c7;
283
+ color: #d97706;
284
+ border-color: #fde047;
285
+ }
286
+ .badge.danger {
287
+ background: #fee2e2;
288
+ color: #b91c1c;
289
+ border-color: #fca5a5;
290
+ }
291
+ .badge.info {
292
+ background: #dbeafe;
293
+ color: #1e40af;
294
+ border-color: #93c5fd;
295
+ }
296
+ .list {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 10px;
300
+ }
301
+ .list-item {
302
+ padding: 12px;
303
+ border-radius: 8px;
304
+ border: 1px solid var(--border);
305
+ background: var(--card);
306
+ transition: all 0.15s ease;
307
+ }
308
+ .list-item:hover {
309
+ background: #f9fafb;
310
+ border-color: var(--primary);
311
+ transform: translateX(2px);
312
+ box-shadow: 0 2px 8px rgba(37,99,235,0.1);
313
+ }
314
+ .list-item:last-child {
315
+ margin-bottom: 0;
316
+ }
317
+ .resource-search { display: flex; gap: 12px; }
318
+ .resource-search input {
319
+ flex: 1;
320
+ padding: 10px 12px;
321
+ border-radius: 6px;
322
+ border: 1px solid var(--border);
323
+ font-size: 14px;
324
+ }
325
+ .resource-columns {
326
+ display: grid;
327
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
328
+ gap: 12px;
329
+ margin-top: 16px;
330
+ }
331
+ .resource-item {
332
+ padding: 10px;
333
+ border: 1px solid var(--border);
334
+ border-radius: 6px;
335
+ margin-bottom: 8px;
336
+ font-size: 13px;
337
+ }
338
+ .form-grid {
339
+ display: grid;
340
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
341
+ gap: 10px;
342
+ }
343
+ label { font-size: 12px; font-weight: 600; color: var(--muted); }
344
+ input, textarea, select {
345
+ width: 100%;
346
+ padding: 10px 14px;
347
+ border: 1px solid var(--border);
348
+ border-radius: 8px;
349
+ font-size: 14px;
350
+ font-family: inherit;
351
+ background: var(--card);
352
+ color: var(--text);
353
+ transition: all 0.2s ease;
354
+ }
355
+ input:focus, textarea:focus, select:focus {
356
+ outline: none;
357
+ border-color: var(--primary);
358
+ box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
359
+ }
360
+ input:hover, textarea:hover, select:hover {
361
+ border-color: rgba(37,99,235,0.3);
362
+ }
363
+ textarea {
364
+ min-height: 120px;
365
+ resize: vertical;
366
+ font-family: 'Monaco', 'Courier New', monospace;
367
+ font-size: 13px;
368
+ }
369
+ .toast-container {
370
+ position: fixed;
371
+ bottom: 20px;
372
+ right: 20px;
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 10px;
376
+ }
377
+ .toast {
378
+ background: #111827;
379
+ color: white;
380
+ padding: 12px 16px;
381
+ border-radius: 6px;
382
+ min-width: 240px;
383
+ box-shadow: 0 8px 20px rgba(15,23,42,0.25);
384
+ }
385
+ @media (max-width: 720px) {
386
+ .kpi-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
387
+ .grid-2 { grid-template-columns: 1fr; }
388
+ .resource-search { flex-direction: column; }
389
+ }
390
+ </style>
391
+ </head>
392
+ <body>
393
+ <div class="toast-container" id="toastContainer"></div>
394
+ <div class="container">
395
+ <div class="header">
396
+ <div class="logo">
397
+ <h1>🚀 Crypto API Monitor</h1>
398
+ <div style="font-size:12px; color:var(--muted);">Real API diagnostics + market intelligence</div>
399
+ </div>
400
+ <div class="header-actions">
401
+ <div class="status-pill" id="wsStatus">Connecting...</div>
402
+ <button class="btn secondary" onclick="refreshAll()">Refresh</button>
403
+ <button class="btn" onclick="loadErrorSummary()">Diagnostics</button>
404
+ </div>
405
+ </div>
406
+
407
+ <div class="kpi-grid">
408
+ <div class="kpi-card">
409
+ <div class="kpi-header">
410
+ <div class="kpi-label">Total APIs</div>
411
+ <div class="kpi-icon blue">
412
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
413
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
414
+ </svg>
415
+ </div>
416
+ </div>
417
+ <div class="kpi-value" id="kpiTotalAPIs">--</div>
418
+ <div class="kpi-trend" id="kpiTotalTrend">Loading…</div>
419
+ </div>
420
+ <div class="kpi-card">
421
+ <div class="kpi-header">
422
+ <div class="kpi-label">Online</div>
423
+ <div class="kpi-icon green">
424
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
425
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
426
+ </svg>
427
+ </div>
428
+ </div>
429
+ <div class="kpi-value" id="kpiOnline">--</div>
430
+ <div class="kpi-trend" id="kpiOnlineTrend">Loading…</div>
431
+ </div>
432
+ <div class="kpi-card">
433
+ <div class="kpi-header">
434
+ <div class="kpi-label">Avg Response</div>
435
+ <div class="kpi-icon orange">
436
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
437
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
438
+ </svg>
439
+ </div>
440
+ </div>
441
+ <div class="kpi-value" id="kpiAvgResponse">--</div>
442
+ <div class="kpi-trend" id="kpiResponseTrend">Loading…</div>
443
+ </div>
444
+ <div class="kpi-card">
445
+ <div class="kpi-header">
446
+ <div class="kpi-label">Last Update</div>
447
+ <div class="kpi-icon purple">
448
+ <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
449
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
450
+ </svg>
451
+ </div>
452
+ </div>
453
+ <div class="kpi-value" id="kpiLastUpdate" style="font-size:16px;">--</div>
454
+ <div class="kpi-trend">Auto-refresh</div>
455
+ </div>
456
+ </div>
457
+
458
+ <div class="tabs" id="tabBar">
459
+ <button class="tab active" data-tab="dashboard" onclick="switchTab(event, 'dashboard')">Dashboard</button>
460
+ <button class="tab" data-tab="providers" onclick="switchTab(event, 'providers')">Providers</button>
461
+ <button class="tab" data-tab="market" onclick="switchTab(event, 'market')">Market</button>
462
+ <button class="tab" data-tab="sentiment" onclick="switchTab(event, 'sentiment')">Sentiment</button>
463
+ <button class="tab" data-tab="news" onclick="switchTab(event, 'news')">News</button>
464
+ <button class="tab" data-tab="resources" onclick="switchTab(event, 'resources')">Resources & Tools</button>
465
+ </div>
466
+
467
+ <div class="tab-content active" id="tab-dashboard">
468
+ <div class="grid grid-2">
469
+ <div class="card">
470
+ <div class="card-header">
471
+ <h3 class="card-title">Provider Overview</h3>
472
+ <button class="btn secondary" onclick="loadProviders()">Reload</button>
473
+ </div>
474
+ <div class="table-wrapper" style="overflow:auto;">
475
+ <table>
476
+ <thead>
477
+ <tr><th>Provider</th><th>Status</th><th>Response</th><th>Uptime</th></tr>
478
+ </thead>
479
+ <tbody id="providersTableBody">
480
+ <tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">Loading providers…</td></tr>
481
+ </tbody>
482
+ </table>
483
+ </div>
484
+ </div>
485
+ <div class="card">
486
+ <div class="card-header">
487
+ <h3 class="card-title">Error Monitor</h3>
488
+ <button class="btn secondary" onclick="loadErrorSummary()">Refresh</button>
489
+ </div>
490
+ <div id="errorSummaryCard" style="font-size:13px; color:var(--muted);">Gathering diagnostics…</div>
491
+ <div id="diagnosticsList" class="list" style="margin-top:12px;"></div>
492
+ </div>
493
+ </div>
494
+ <div class="grid grid-2">
495
+ <div class="card">
496
+ <div class="card-header"><h3 class="card-title">Health Trend</h3></div>
497
+ <div style="height:260px;"><canvas id="healthChart"></canvas></div>
498
+ </div>
499
+ <div class="card">
500
+ <div class="card-header"><h3 class="card-title">Status Distribution</h3></div>
501
+ <div style="height:260px;"><canvas id="statusChart"></canvas></div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="tab-content" id="tab-providers">
507
+ <div class="card">
508
+ <div class="card-header">
509
+ <h3 class="card-title">Providers Detail</h3>
510
+ <button class="btn secondary" onclick="loadProviders()">Reload</button>
511
+ </div>
512
+ <div id="providersDetail"></div>
513
+ </div>
514
+ </div>
515
+
516
+ <div class="tab-content" id="tab-market">
517
+ <div class="grid grid-2">
518
+ <div class="card">
519
+ <div class="card-header"><h3 class="card-title">Global Stats</h3></div>
520
+ <div id="marketGlobalStats" class="list"></div>
521
+ </div>
522
+ <div class="card">
523
+ <div class="card-header"><h3 class="card-title">Top Movers</h3></div>
524
+ <div style="height:260px;"><canvas id="marketChart"></canvas></div>
525
+ </div>
526
+ </div>
527
+ <div class="grid grid-2">
528
+ <div class="card">
529
+ <div class="card-header"><h3 class="card-title">Top Assets</h3></div>
530
+ <div class="table-wrapper" style="overflow:auto; max-height:320px;">
531
+ <table>
532
+ <thead><tr><th>Rank</th><th>Name</th><th>Price</th><th>24h</th><th>Market Cap</th></tr></thead>
533
+ <tbody id="marketTableBody"></tbody>
534
+ </table>
535
+ </div>
536
+ </div>
537
+ <div class="card">
538
+ <div class="card-header"><h3 class="card-title">Trending Now</h3></div>
539
+ <div id="trendingList" class="list"></div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="tab-content" id="tab-sentiment">
545
+ <div class="grid grid-2">
546
+ <div class="card">
547
+ <div class="card-header"><h3 class="card-title">Fear & Greed Index</h3></div>
548
+ <div id="sentimentCard" style="font-size:14px; color:var(--muted);">Loading sentiment…</div>
549
+ </div>
550
+ <div class="card">
551
+ <div class="card-header"><h3 class="card-title">DeFi TVL</h3></div>
552
+ <div class="table-wrapper" style="overflow:auto; max-height:280px;">
553
+ <table>
554
+ <thead><tr><th>Protocol</th><th>TVL</th><th>24h</th><th>Chain</th></tr></thead>
555
+ <tbody id="defiTableBody"></tbody>
556
+ </table>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+
562
+ <div class="tab-content" id="tab-news">
563
+ <div class="card">
564
+ <div class="card-header">
565
+ <h3 class="card-title">Latest Headlines</h3>
566
+ <button class="btn secondary" onclick="loadNews()">Reload</button>
567
+ </div>
568
+ <div id="newsList" class="list"></div>
569
+ </div>
570
+ </div>
571
+
572
+ <div class="tab-content" id="tab-resources">
573
+ <div class="card">
574
+ <div class="card-header">
575
+ <h3 class="card-title">Resource Search</h3>
576
+ <span style="font-size:12px;color:var(--muted);">Live search across providers + HuggingFace registry</span>
577
+ </div>
578
+ <div class="resource-search">
579
+ <input type="text" id="resourceSearch" placeholder="Search provider, model or dataset..." />
580
+ <select id="resourceFilter" onchange="loadResourceSearch()">
581
+ <option value="all">All sources</option>
582
+ <option value="providers">Providers</option>
583
+ <option value="models">Models</option>
584
+ <option value="datasets">Datasets</option>
585
+ </select>
586
+ </div>
587
+ <div class="resource-columns">
588
+ <div>
589
+ <h4>Providers <span id="resourceCountProviders" style="color:var(--muted);"></span></h4>
590
+ <div id="resourceResultsProviders"></div>
591
+ </div>
592
+ <div>
593
+ <h4>Models <span id="resourceCountModels" style="color:var(--muted);"></span></h4>
594
+ <div id="resourceResultsModels"></div>
595
+ </div>
596
+ <div>
597
+ <h4>Datasets <span id="resourceCountDatasets" style="color:var(--muted);"></span></h4>
598
+ <div id="resourceResultsDatasets"></div>
599
+ </div>
600
+ </div>
601
+ </div>
602
+ <div class="grid grid-2">
603
+ <div class="card">
604
+ <div class="card-header"><h3 class="card-title">Export & Backup</h3></div>
605
+ <div style="display:flex; gap:10px; flex-wrap:wrap;">
606
+ <button class="btn" onclick="handleExport('json')">Export JSON Snapshot</button>
607
+ <button class="btn" onclick="handleExport('csv')">Export CSV</button>
608
+ <button class="btn secondary" onclick="handleBackup()">Create Backup</button>
609
+ </div>
610
+ <div id="exportHistory" class="list" style="margin-top:12px;"></div>
611
+ </div>
612
+ <div class="card">
613
+ <div class="card-header"><h3 class="card-title">Import Provider</h3></div>
614
+ <form id="importForm" onsubmit="handleImportSingle(event)">
615
+ <div class="form-grid">
616
+ <div><label>Name</label><input name="name" required></div>
617
+ <div><label>Category</label><input name="category" required></div>
618
+ <div><label>Endpoint URL</label><input name="endpoint_url" required></div>
619
+ <div><label>Health Endpoint</label><input name="health_check_endpoint"></div>
620
+ <div><label>Rate Limit</label><input name="rate_limit"></div>
621
+ <div><label>Timeout (ms)</label><input name="timeout_ms" type="number" value="10000"></div>
622
+ </div>
623
+ <label style="margin-top:12px; display:block;">Notes<textarea name="notes"></textarea></label>
624
+ <div style="margin-top:12px; display:flex; gap:10px; align-items:center;">
625
+ <label style="display:flex; gap:6px; align-items:center; font-size:13px;">
626
+ <input type="checkbox" name="requires_key"> Requires API Key
627
+ </label>
628
+ <input name="api_key" placeholder="API Key (optional)" style="flex:1;">
629
+ </div>
630
+ <button class="btn" style="margin-top:12px;" type="submit">Import Provider</button>
631
+ </form>
632
+ <hr style="margin:20px 0; border:none; border-top:1px solid var(--border);">
633
+ <label style="display:block; font-size:13px; color:var(--muted); margin-bottom:6px;">Bulk JSON Import</label>
634
+ <textarea id="bulkImportTextarea" placeholder='[{"name":"Sample API","category":"market","endpoint_url":"https://..."}]'></textarea>
635
+ <button class="btn secondary" style="margin-top:10px;" onclick="handleImportBulk()">Run Bulk Import</button>
636
+ </div>
637
+ </div>
638
+ </div>
639
+ </div>
640
+
641
+ <script>
642
+ const config = {
643
+ apiBaseUrl: '',
644
+ wsUrl: (() => {
645
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
646
+ return `${protocol}//${window.location.host}/ws`;
647
+ })(),
648
+ autoRefreshInterval: 30000
649
+ };
650
+
651
+ const state = {
652
+ ws: null,
653
+ wsConnected: false,
654
+ providers: [],
655
+ market: { cryptocurrencies: [], global: {} },
656
+ trending: [],
657
+ sentiment: null,
658
+ defi: [],
659
+ news: [],
660
+ errorSummary: null,
661
+ diagnostics: null,
662
+ resources: { providers: [], models: [], datasets: [] },
663
+ exports: [],
664
+ charts: { health: null, status: null, market: null },
665
+ currentTab: 'dashboard',
666
+ resourceSearchTimeout: null,
667
+ lastUpdate: null
668
+ };
669
+
670
+ function showToast(message, type = 'info') {
671
+ const container = document.getElementById('toastContainer');
672
+ const toast = document.createElement('div');
673
+ toast.className = 'toast';
674
+ toast.style.background = type === 'error' ? '#b91c1c' : (type === 'success' ? '#065f46' : '#111827');
675
+ toast.textContent = message;
676
+ container.appendChild(toast);
677
+ setTimeout(() => toast.remove(), 3000);
678
+ }
679
+
680
+ async function apiCall(endpoint, options = {}) {
681
+ const response = await fetch(config.apiBaseUrl + endpoint, options);
682
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
683
+ return await response.json();
684
+ }
685
+
686
+ function initializeWebSocket() {
687
+ try {
688
+ state.ws = new WebSocket(config.wsUrl);
689
+ state.ws.onopen = () => {
690
+ state.wsConnected = true;
691
+ const pill = document.getElementById('wsStatus');
692
+ pill.textContent = 'Connected';
693
+ pill.style.background = '#dcfce7';
694
+ pill.style.color = '#15803d';
695
+ };
696
+ state.ws.onclose = () => {
697
+ state.wsConnected = false;
698
+ const pill = document.getElementById('wsStatus');
699
+ pill.textContent = 'Disconnected';
700
+ pill.style.background = '#fee2e2';
701
+ pill.style.color = '#b91c1c';
702
+ };
703
+ state.ws.onmessage = (event) => {
704
+ const data = JSON.parse(event.data);
705
+ if (data.type === 'status_update' && data.data?.providers) {
706
+ updateKPIs(data.data.providers);
707
+ }
708
+ if (data.type === 'market_update' && Array.isArray(data.data)) {
709
+ state.market.cryptocurrencies = data.data;
710
+ renderMarketTable();
711
+ updateMarketChart();
712
+ }
713
+ if (data.type === 'sentiment_update') {
714
+ state.sentiment = data.data;
715
+ renderSentiment();
716
+ }
717
+ };
718
+ } catch (err) {
719
+ console.error('WebSocket error', err);
720
+ }
721
+ }
722
+
723
+ async function loadInitialData() {
724
+ try {
725
+ await Promise.all([
726
+ loadProviders(),
727
+ loadMarket(),
728
+ loadTrending(),
729
+ loadSentimentData(),
730
+ loadDefi(),
731
+ loadErrorSummary(),
732
+ loadNews(),
733
+ loadResourceSearch()
734
+ ]);
735
+ initializeCharts();
736
+ state.lastUpdate = new Date();
737
+ updateLastUpdateDisplay();
738
+ showToast('Dashboard ready', 'success');
739
+ } catch (err) {
740
+ console.error(err);
741
+ showToast('Failed to load initial data', 'error');
742
+ }
743
+ }
744
+
745
+ async function loadProviders() {
746
+ try {
747
+ const data = await apiCall('/api/providers');
748
+ state.providers = Array.isArray(data) ? data : [];
749
+ renderProvidersTable();
750
+ renderProvidersDetail();
751
+ updateStatusChart();
752
+ updateKPIs(state.providers);
753
+ } catch (err) {
754
+ console.error(err);
755
+ document.getElementById('providersTableBody').innerHTML = `<tr><td colspan="4" style="padding:40px; text-align:center;">${err.message}</td></tr>`;
756
+ }
757
+ }
758
+
759
+ function renderProvidersTable() {
760
+ const tbody = document.getElementById('providersTableBody');
761
+ if (!state.providers.length) {
762
+ tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No providers available</td></tr>';
763
+ return;
764
+ }
765
+ tbody.innerHTML = state.providers.slice(0, 8).map(p => `
766
+ <tr>
767
+ <td>
768
+ <div style="font-weight:600;">${p.name || 'Unknown'}</div>
769
+ <div style="font-size:11px;color:var(--muted);">${p.category || 'general'}</div>
770
+ </td>
771
+ <td>${renderStatusBadge(p.status)}</td>
772
+ <td>${p.response_time_ms ? `${p.response_time_ms}ms` : '--'}</td>
773
+ <td>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</td>
774
+ </tr>
775
+ `).join('');
776
+ }
777
+
778
+ function renderProvidersDetail() {
779
+ const container = document.getElementById('providersDetail');
780
+ if (!state.providers.length) {
781
+ container.innerHTML = '<div style="padding:40px; text-align:center; color:var(--muted);">No providers data available</div>';
782
+ return;
783
+ }
784
+ container.innerHTML = `
785
+ <div class="table-wrapper">
786
+ <table>
787
+ <thead>
788
+ <tr>
789
+ <th>Name</th>
790
+ <th>Status</th>
791
+ <th>Response Time</th>
792
+ <th>Success Rate</th>
793
+ <th>Rate Limit</th>
794
+ </tr>
795
+ </thead>
796
+ <tbody>
797
+ ${state.providers.map(p => `
798
+ <tr>
799
+ <td>
800
+ <strong>${p.name || 'Unknown'}</strong>
801
+ <div style="font-size:11px;color:var(--muted);margin-top:2px;">${p.category || 'general'}</div>
802
+ </td>
803
+ <td>${renderStatusBadge(p.status)}</td>
804
+ <td><strong>${p.avg_response_time_ms ? `${p.avg_response_time_ms}ms` : (p.response_time_ms ? `${p.response_time_ms}ms` : '--')}</strong></td>
805
+ <td><strong>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</strong></td>
806
+ <td style="font-size:12px;color:var(--muted);">${p.rate_limit || '—'}</td>
807
+ </tr>`).join('')}
808
+ </tbody>
809
+ </table>
810
+ </div>`;
811
+ }
812
+
813
+ function renderStatusBadge(status = 'unknown') {
814
+ const normalized = (status || '').toLowerCase();
815
+ let cls = 'badge warn';
816
+ if (['online', 'healthy'].includes(normalized)) cls = 'badge success';
817
+ if (['offline', 'error'].includes(normalized)) cls = 'badge danger';
818
+ return `<span class="${cls}">${status || 'unknown'}</span>`;
819
+ }
820
+
821
+ function updateKPIs(data) {
822
+ const providers = Array.isArray(data) ? data : (data?.providers || []);
823
+ const total = providers.length;
824
+ const online = providers.filter(p => ['online', 'healthy'].includes((p.status || '').toLowerCase())).length;
825
+ const responseTimes = providers.map(p => p.response_time_ms || p.avg_response_time_ms).filter(Boolean);
826
+ const avgResponse = responseTimes.length ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0;
827
+
828
+ document.getElementById('kpiTotalAPIs').textContent = total;
829
+ document.getElementById('kpiTotalTrend').textContent = `${total} tracked providers`;
830
+ document.getElementById('kpiOnline').textContent = online;
831
+ document.getElementById('kpiOnlineTrend').textContent = total ? `${Math.round((online / total) * 100)}% uptime` : 'No data';
832
+ document.getElementById('kpiAvgResponse').textContent = avgResponse ? `${avgResponse}ms` : '--';
833
+ document.getElementById('kpiResponseTrend').textContent = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
834
+ updateHealthChart(avgResponse);
835
+ }
836
+
837
+ function updateLastUpdateDisplay() {
838
+ if (!state.lastUpdate) return;
839
+ document.getElementById('kpiLastUpdate').textContent = state.lastUpdate.toLocaleTimeString();
840
+ }
841
+
842
+ function initializeCharts() {
843
+ const healthCtx = document.getElementById('healthChart').getContext('2d');
844
+ const statusCtx = document.getElementById('statusChart').getContext('2d');
845
+ const marketCtx = document.getElementById('marketChart').getContext('2d');
846
+
847
+ if (state.charts.health) state.charts.health.destroy();
848
+ if (state.charts.status) state.charts.status.destroy();
849
+ if (state.charts.market) state.charts.market.destroy();
850
+
851
+ state.charts.health = new Chart(healthCtx, {
852
+ type: 'line',
853
+ data: { labels: [], datasets: [{ label: 'Avg Response (ms)', data: [], borderColor: '#2563eb', fill: false }] },
854
+ options: { responsive: true, maintainAspectRatio: false }
855
+ });
856
+
857
+ state.charts.status = new Chart(statusCtx, {
858
+ type: 'doughnut',
859
+ data: { labels: ['Online', 'Degraded', 'Offline'], datasets: [{ data: [0, 0, 0], backgroundColor: ['#16a34a', '#fcd34d', '#f87171'] }] },
860
+ options: { responsive: true, maintainAspectRatio: false }
861
+ });
862
+
863
+ state.charts.market = new Chart(marketCtx, {
864
+ type: 'bar',
865
+ data: { labels: [], datasets: [{ label: 'Market Cap (B USD)', data: [], backgroundColor: '#93c5fd' }] },
866
+ options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } }
867
+ });
868
+ }
869
+
870
+ function updateHealthChart(value) {
871
+ if (!state.charts.health || !value) return;
872
+ const chart = state.charts.health;
873
+ chart.data.labels.push(new Date().toLocaleTimeString());
874
+ chart.data.datasets[0].data.push(value);
875
+ if (chart.data.labels.length > 12) {
876
+ chart.data.labels.shift();
877
+ chart.data.datasets[0].data.shift();
878
+ }
879
+ chart.update();
880
+ }
881
+
882
+ function updateStatusChart() {
883
+ if (!state.charts.status) return;
884
+ const online = state.providers.filter(p => (p.status || '').toLowerCase() === 'online').length;
885
+ const degraded = state.providers.filter(p => (p.status || '').toLowerCase() === 'degraded').length;
886
+ const offline = state.providers.length - online - degraded;
887
+ state.charts.status.data.datasets[0].data = [online, degraded, offline];
888
+ state.charts.status.update();
889
+ }
890
+
891
+ async function loadMarket() {
892
+ const data = await apiCall('/api/market');
893
+ state.market = data;
894
+ renderMarketCards();
895
+ renderMarketTable();
896
+ updateMarketChart();
897
+ }
898
+
899
+ function renderMarketCards() {
900
+ const stats = state.market.global || {};
901
+ const container = document.getElementById('marketGlobalStats');
902
+ container.innerHTML = `
903
+ <div><strong>Total Market Cap:</strong> $${formatNumber(stats.total_market_cap)}</div>
904
+ <div><strong>Total Volume:</strong> $${formatNumber(stats.total_volume)}</div>
905
+ <div><strong>BTC Dominance:</strong> ${stats.btc_dominance ? stats.btc_dominance.toFixed(2) + '%' : '--'}</div>
906
+ <div><strong>ETH Dominance:</strong> ${stats.eth_dominance ? stats.eth_dominance.toFixed(2) + '%' : '--'}</div>
907
+ <div><strong>Active Cryptos:</strong> ${stats.active_cryptocurrencies || '--'}</div>
908
+ <div><strong>Markets:</strong> ${stats.markets || '--'}</div>
909
+ `;
910
+ }
911
+
912
+ function renderMarketTable() {
913
+ const tbody = document.getElementById('marketTableBody');
914
+ const coins = state.market.cryptocurrencies || [];
915
+ if (!coins.length) {
916
+ tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:40px; color:var(--muted);">Market data unavailable</td></tr>';
917
+ return;
918
+ }
919
+ tbody.innerHTML = coins.slice(0, 12).map(coin => `
920
+ <tr>
921
+ <td>${coin.rank || coin.market_cap_rank || '-'}</td>
922
+ <td>${coin.name} <span style="color:var(--muted);">${coin.symbol}</span></td>
923
+ <td>$${formatNumber(coin.price)}</td>
924
+ <td style="color:${coin.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${coin.change_24h ? coin.change_24h.toFixed(2) : '0'}%</td>
925
+ <td>$${formatNumber(coin.market_cap)}</td>
926
+ </tr>
927
+ `).join('');
928
+ }
929
+
930
+ function updateMarketChart() {
931
+ if (!state.charts.market) return;
932
+ const coins = state.market.cryptocurrencies || [];
933
+ const top = coins.slice(0, 5);
934
+ state.charts.market.data.labels = top.map(c => c.name);
935
+ state.charts.market.data.datasets[0].data = top.map(c => c.market_cap ? (c.market_cap / 1e9).toFixed(2) : 0);
936
+ state.charts.market.update();
937
+ }
938
+
939
+ async function loadTrending() {
940
+ const data = await apiCall('/api/trending');
941
+ state.trending = data.trending || [];
942
+ const list = document.getElementById('trendingList');
943
+ if (!state.trending.length) {
944
+ list.innerHTML = '<div class="list-item" style="color:var(--muted);">No trending assets</div>';
945
+ return;
946
+ }
947
+ list.innerHTML = state.trending.map(item => `
948
+ <div class="list-item">
949
+ <div style="font-weight:600;">${item.name} (${item.symbol})</div>
950
+ <div style="font-size:12px;color:var(--muted);">Rank: ${item.rank || '—'}</div>
951
+ </div>`).join('');
952
+ }
953
+
954
+ async function loadSentimentData() {
955
+ const data = await apiCall('/api/sentiment');
956
+ state.sentiment = data.fear_greed_index;
957
+ renderSentiment();
958
+ }
959
+
960
+ function renderSentiment() {
961
+ const container = document.getElementById('sentimentCard');
962
+ if (!state.sentiment) {
963
+ container.textContent = 'No sentiment data';
964
+ return;
965
+ }
966
+ const timestamp = Number(state.sentiment.timestamp);
967
+ container.innerHTML = `
968
+ <div style="font-size:32px; font-weight:700;">${state.sentiment.value}</div>
969
+ <div style="font-size:14px; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">${state.sentiment.classification}</div>
970
+ <div style="font-size:12px; color:var(--muted);">Updated: ${timestamp ? new Date(timestamp * 1000).toLocaleString() : '--'}</div>
971
+ `;
972
+ }
973
+
974
+ async function loadDefi() {
975
+ const data = await apiCall('/api/defi');
976
+ state.defi = data.protocols || [];
977
+ const tbody = document.getElementById('defiTableBody');
978
+ if (!state.defi.length) {
979
+ tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No DeFi data</td></tr>';
980
+ return;
981
+ }
982
+ tbody.innerHTML = state.defi.slice(0, 10).map(proto => `
983
+ <tr>
984
+ <td>${proto.name}</td>
985
+ <td>$${formatNumber(proto.tvl)}</td>
986
+ <td style="color:${proto.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${proto.change_24h ? proto.change_24h.toFixed(2) : 0}%</td>
987
+ <td>${proto.chain || '—'}</td>
988
+ </tr>`).join('');
989
+ }
990
+
991
+ async function loadNews() {
992
+ const data = await apiCall('/api/news');
993
+ state.news = data.articles || [];
994
+ const list = document.getElementById('newsList');
995
+ if (!state.news.length) {
996
+ list.innerHTML = '<div class="list-item" style="color:var(--muted);">No news available</div>';
997
+ return;
998
+ }
999
+ list.innerHTML = state.news.slice(0, 12).map(article => `
1000
+ <div class="news-item list-item">
1001
+ <h4>${article.title}</h4>
1002
+ <div class="news-meta">${article.source || 'Unknown'} • ${article.published_at ? new Date(article.published_at).toLocaleString() : ''}</div>
1003
+ <p style="margin:6px 0;">${article.description || ''}</p>
1004
+ <a href="${article.link}" target="_blank" style="font-size:12px; color:var(--primary);">Read article →</a>
1005
+ </div>`).join('');
1006
+ }
1007
+
1008
+ async function loadErrorSummary() {
1009
+ try {
1010
+ const [summary, diagnostics] = await Promise.all([
1011
+ apiCall('/api/logs/summary'),
1012
+ apiCall('/api/diagnostics/errors')
1013
+ ]);
1014
+ state.errorSummary = summary;
1015
+ state.diagnostics = diagnostics;
1016
+ renderErrorSummary();
1017
+ } catch (err) {
1018
+ console.error(err);
1019
+ document.getElementById('errorSummaryCard').textContent = 'Failed to load diagnostics';
1020
+ }
1021
+ }
1022
+
1023
+ function renderErrorSummary() {
1024
+ const summary = state.errorSummary;
1025
+ const card = document.getElementById('errorSummaryCard');
1026
+ if (!summary) {
1027
+ card.textContent = 'No diagnostics available';
1028
+ return;
1029
+ }
1030
+ card.innerHTML = `
1031
+ <div><strong>Total Logs:</strong> ${summary.total}</div>
1032
+ <div><strong>Last Error:</strong> ${summary.last_error ? summary.last_error.provider + ' @ ' + summary.last_error.timestamp : 'None'}</div>
1033
+ <div><strong>Top Offenders:</strong> ${Object.keys(summary.by_provider || {}).slice(0,3).join(', ') || '—'}</div>
1034
+ `;
1035
+ const diag = state.diagnostics || { recent: [] };
1036
+ const list = document.getElementById('diagnosticsList');
1037
+ list.innerHTML = diag.recent.slice(0,5).map(item => `
1038
+ <div class="list-item">
1039
+ <div style="font-weight:600;">${item.provider}</div>
1040
+ <div style="font-size:12px; color:var(--muted);">${item.timestamp}</div>
1041
+ <div style="font-size:13px; color:${item.status === 'offline' ? '#dc2626' : '#d97706'};">${item.message || 'No message'}</div>
1042
+ </div>`).join('');
1043
+ }
1044
+
1045
+ async function loadResourceSearch() {
1046
+ const query = document.getElementById('resourceSearch')?.value || '';
1047
+ const source = document.getElementById('resourceFilter').value;
1048
+ const data = await apiCall(`/api/resources/search?q=${encodeURIComponent(query)}&source=${source}`);
1049
+ state.resources = data.results;
1050
+ document.getElementById('resourceCountProviders').textContent = `(${data.counts.providers})`;
1051
+ document.getElementById('resourceCountModels').textContent = `(${data.counts.models})`;
1052
+ document.getElementById('resourceCountDatasets').textContent = `(${data.counts.datasets})`;
1053
+ renderResourceResults();
1054
+ }
1055
+
1056
+ function renderResourceResults() {
1057
+ const providersContainer = document.getElementById('resourceResultsProviders');
1058
+ const modelsContainer = document.getElementById('resourceResultsModels');
1059
+ const datasetsContainer = document.getElementById('resourceResultsDatasets');
1060
+
1061
+ providersContainer.innerHTML = state.resources.providers.slice(0,6).map(p => `
1062
+ <div class="resource-item">
1063
+ <strong>${p.name}</strong>
1064
+ <div style="font-size:12px;color:var(--muted);">${p.category}</div>
1065
+ <div style="font-size:12px;">Status: ${p.status}</div>
1066
+ </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1067
+
1068
+ modelsContainer.innerHTML = state.resources.models.slice(0,6).map(m => `
1069
+ <div class="resource-item">
1070
+ <strong>${m.id}</strong>
1071
+ <div style="font-size:12px;color:var(--muted);">${m.description || 'No description'}</div>
1072
+ </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1073
+
1074
+ datasetsContainer.innerHTML = state.resources.datasets.slice(0,6).map(d => `
1075
+ <div class="resource-item">
1076
+ <strong>${d.id}</strong>
1077
+ <div style="font-size:12px;color:var(--muted);">${d.description || 'No description'}</div>
1078
+ </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1079
+ }
1080
+
1081
+ async function handleExport(type) {
1082
+ try {
1083
+ const res = await apiCall(`/api/v2/export/${type}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
1084
+ state.exports.unshift({ type, url: res.download_url, timestamp: res.timestamp });
1085
+ renderExportHistory();
1086
+ showToast(`${type.toUpperCase()} export ready`, 'success');
1087
+ } catch (err) {
1088
+ console.error(err);
1089
+ showToast('Export failed', 'error');
1090
+ }
1091
+ }
1092
+
1093
+ async function handleBackup() {
1094
+ try {
1095
+ const res = await apiCall('/api/v2/backup', { method: 'POST' });
1096
+ state.exports.unshift({ type: 'backup', url: res.download_url, timestamp: res.timestamp });
1097
+ renderExportHistory();
1098
+ showToast('Backup created', 'success');
1099
+ } catch (err) {
1100
+ console.error(err);
1101
+ showToast('Backup failed', 'error');
1102
+ }
1103
+ }
1104
+
1105
+ function renderExportHistory() {
1106
+ const container = document.getElementById('exportHistory');
1107
+ if (!state.exports.length) {
1108
+ container.innerHTML = '<div style="color:var(--muted); font-size:13px;">No exports yet</div>';
1109
+ return;
1110
+ }
1111
+ container.innerHTML = state.exports.slice(0,5).map(entry => `
1112
+ <div class="list-item" style="border-bottom:1px solid var(--border);">
1113
+ <div style="font-weight:600;">${entry.type.toUpperCase()}</div>
1114
+ <div style="font-size:12px; color:var(--muted);">${new Date(entry.timestamp).toLocaleString()}</div>
1115
+ <a href="${entry.url}" style="font-size:12px; color:var(--primary);" target="_blank">Download</a>
1116
+ </div>`).join('');
1117
+ }
1118
+
1119
+ async function handleImportSingle(event) {
1120
+ event.preventDefault();
1121
+ const form = event.target;
1122
+ const payload = Object.fromEntries(new FormData(form).entries());
1123
+ payload.requires_key = form.elements['requires_key'].checked;
1124
+ payload.timeout_ms = Number(payload.timeout_ms) || 10000;
1125
+ try {
1126
+ await apiCall('/api/providers', {
1127
+ method: 'POST',
1128
+ headers: { 'Content-Type': 'application/json' },
1129
+ body: JSON.stringify(payload)
1130
+ });
1131
+ showToast('Provider imported', 'success');
1132
+ form.reset();
1133
+ loadProviders();
1134
+ } catch (err) {
1135
+ console.error(err);
1136
+ showToast('Import failed', 'error');
1137
+ }
1138
+ }
1139
+
1140
+ async function handleImportBulk() {
1141
+ const textarea = document.getElementById('bulkImportTextarea');
1142
+ if (!textarea.value.trim()) {
1143
+ showToast('Paste provider JSON first', 'error');
1144
+ return;
1145
+ }
1146
+ try {
1147
+ const providers = JSON.parse(textarea.value);
1148
+ await apiCall('/api/v2/import/providers', {
1149
+ method: 'POST',
1150
+ headers: { 'Content-Type': 'application/json' },
1151
+ body: JSON.stringify({ providers })
1152
+ });
1153
+ showToast('Bulk import complete', 'success');
1154
+ textarea.value = '';
1155
+ loadProviders();
1156
+ } catch (err) {
1157
+ console.error(err);
1158
+ showToast('Bulk import failed', 'error');
1159
+ }
1160
+ }
1161
+
1162
+ function switchTab(event, tabName) {
1163
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
1164
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
1165
+ event.currentTarget.classList.add('active');
1166
+ document.getElementById(`tab-${tabName}`).classList.add('active');
1167
+ state.currentTab = tabName;
1168
+ if (tabName === 'market') loadMarket();
1169
+ if (tabName === 'sentiment') { loadSentimentData(); loadDefi(); }
1170
+ if (tabName === 'news') loadNews();
1171
+ }
1172
+
1173
+ function startAutoRefresh() {
1174
+ setInterval(() => {
1175
+ if (state.wsConnected) {
1176
+ refreshAll();
1177
+ }
1178
+ }, config.autoRefreshInterval);
1179
+ }
1180
+
1181
+ function refreshAll() {
1182
+ loadProviders();
1183
+ loadMarket();
1184
+ loadTrending();
1185
+ loadSentimentData();
1186
+ loadDefi();
1187
+ loadErrorSummary();
1188
+ loadNews();
1189
+ loadResourceSearch();
1190
+ }
1191
+
1192
+ function formatNumber(value) {
1193
+ if (!value && value !== 0) return '--';
1194
+ if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
1195
+ if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
1196
+ if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
1197
+ if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K';
1198
+ return Number(value).toFixed(2);
1199
+ }
1200
+
1201
+ function setupResourceSearch() {
1202
+ const input = document.getElementById('resourceSearch');
1203
+ input.addEventListener('input', () => {
1204
+ clearTimeout(state.resourceSearchTimeout);
1205
+ state.resourceSearchTimeout = setTimeout(loadResourceSearch, 400);
1206
+ });
1207
+ }
1208
+
1209
+ initializeWebSocket();
1210
+ setupResourceSearch();
1211
+ loadInitialData();
1212
+ startAutoRefresh();
1213
+ </script>
1214
+ </body>
1215
+ </html>
1216
+
package.json CHANGED
@@ -10,12 +10,7 @@
10
  "dashboard": "python3 -m http.server 8080",
11
  "full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
12
  "test:free-resources": "node free_resources_selftest.mjs",
13
- "test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1",
14
- "test:theme": "node tests/verify_theme.js",
15
- "test:api-client": "node tests/test_apiClient.test.js"
16
- },
17
- "devDependencies": {
18
- "fast-check": "^3.15.0"
19
  },
20
  "keywords": [
21
  "cryptocurrency",
 
10
  "dashboard": "python3 -m http.server 8080",
11
  "full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
12
  "test:free-resources": "node free_resources_selftest.mjs",
13
+ "test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
 
 
 
 
 
14
  },
15
  "keywords": [
16
  "cryptocurrency",
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/pro-dashboard.css CHANGED
@@ -1,34 +1,22 @@
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, #0a0e1a, #05060a 70%);
5
- --glass-bg: rgba(20, 28, 45, 0.85);
6
- --glass-border: rgba(255, 255, 255, 0.18);
7
- --glass-highlight: rgba(255, 255, 255, 0.25);
8
  --primary: #8f88ff;
9
  --primary-strong: #6c63ff;
10
- --primary-light: #b8b3ff;
11
- --primary-glow: rgba(143, 136, 255, 0.4);
12
  --secondary: #16d9fa;
13
- --secondary-light: #5ee3f5;
14
- --secondary-glow: rgba(22, 217, 250, 0.4);
15
  --accent: #f472b6;
16
- --success: #4ade80;
17
- --success-light: #86efac;
18
- --success-glow: rgba(74, 222, 128, 0.5);
19
- --success-dark: #22c55e;
20
  --warning: #facc15;
21
- --warning-light: #fde047;
22
  --danger: #ef4444;
23
- --danger-light: #f87171;
24
  --info: #38bdf8;
25
- --info-light: #7dd3fc;
26
- --text-primary: #ffffff;
27
- --text-secondary: #e2e8f0;
28
- --text-muted: rgba(226, 232, 240, 0.85);
29
- --text-faint: rgba(226, 232, 240, 0.6);
30
- --shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.7);
31
- --shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.6);
32
  --sidebar-width: 260px;
33
  }
34
 
@@ -40,16 +28,9 @@ html, body {
40
  margin: 0;
41
  padding: 0;
42
  min-height: 100vh;
43
- font-family: 'Manrope', 'DM Sans', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
44
- font-weight: 500;
45
- font-size: 15px;
46
- line-height: 1.65;
47
- letter-spacing: -0.015em;
48
  background: var(--bg-gradient);
49
  color: var(--text-primary);
50
- -webkit-font-smoothing: antialiased;
51
- -moz-osx-font-smoothing: grayscale;
52
- text-rendering: optimizeLegibility;
53
  }
54
 
55
  body[data-theme='light'] {
@@ -68,137 +49,39 @@ body[data-theme='light'] {
68
 
69
  .sidebar {
70
  width: var(--sidebar-width);
71
- padding: 28px 20px;
72
- background: linear-gradient(180deg,
73
- #ffffff 0%,
74
- rgba(219, 234, 254, 0.3) 20%,
75
- rgba(221, 214, 254, 0.25) 40%,
76
- rgba(251, 207, 232, 0.2) 60%,
77
- rgba(221, 214, 254, 0.25) 80%,
78
- rgba(251, 207, 232, 0.15) 90%,
79
- #ffffff 100%);
80
  backdrop-filter: blur(30px);
81
- border-right: 1px solid rgba(0, 0, 0, 0.08);
82
  display: flex;
83
  flex-direction: column;
84
- gap: 28px;
85
  position: sticky;
86
  top: 0;
87
  height: 100vh;
88
- box-shadow: 4px 0 24px rgba(0, 0, 0, 0.08), inset -1px 0 0 rgba(255, 255, 255, 0.5);
89
  }
90
 
91
  .brand {
92
- display: flex;
93
- align-items: center;
94
- gap: 12px;
95
- padding: 14px;
96
- background: rgba(255, 255, 255, 0.03);
97
- border-radius: 14px;
98
- border: 1px solid rgba(255, 255, 255, 0.08);
99
- box-shadow:
100
- inset 0 1px 2px rgba(255, 255, 255, 0.05),
101
- 0 2px 8px rgba(0, 0, 0, 0.2);
102
- position: relative;
103
- overflow: hidden;
104
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
105
- }
106
-
107
- .brand::before {
108
- content: '';
109
- position: absolute;
110
- inset: 0;
111
- background: linear-gradient(135deg, rgba(143, 136, 255, 0.08), rgba(22, 217, 250, 0.05));
112
- opacity: 0;
113
- transition: opacity 0.4s ease;
114
- }
115
-
116
- .brand:hover {
117
- background: rgba(255, 255, 255, 0.05);
118
- border-color: rgba(143, 136, 255, 0.2);
119
- box-shadow:
120
- inset 0 1px 2px rgba(255, 255, 255, 0.08),
121
- 0 4px 16px rgba(143, 136, 255, 0.15);
122
- }
123
-
124
- .brand:hover::before {
125
- opacity: 1;
126
- }
127
-
128
- .brand-icon {
129
- display: flex;
130
- align-items: center;
131
- justify-content: center;
132
- width: 40px;
133
- height: 40px;
134
- border-radius: 10px;
135
- background: rgba(255, 255, 255, 0.04);
136
- border: 1px solid rgba(255, 255, 255, 0.08);
137
- color: var(--text-secondary);
138
- flex-shrink: 0;
139
- box-shadow:
140
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
141
- inset 0 -1px 2px rgba(0, 0, 0, 0.2),
142
- 0 2px 4px rgba(0, 0, 0, 0.2);
143
- position: relative;
144
- }
145
-
146
- .brand-icon::before {
147
- content: '';
148
- position: absolute;
149
- top: 2px;
150
- left: 2px;
151
- right: 2px;
152
- height: 50%;
153
- border-radius: 10px 10px 0 0;
154
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent);
155
- pointer-events: none;
156
- }
157
-
158
- .brand-icon svg {
159
- position: relative;
160
- z-index: 1;
161
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
162
- }
163
-
164
- .brand-text {
165
  display: flex;
166
  flex-direction: column;
167
  gap: 6px;
168
- flex: 1;
169
- min-width: 0;
170
  }
171
 
172
  .brand strong {
173
- font-size: 1rem;
174
- font-weight: 700;
175
- letter-spacing: -0.01em;
176
- font-family: 'Manrope', 'DM Sans', sans-serif;
177
- color: var(--text-primary);
178
- line-height: 1.3;
179
- white-space: nowrap;
180
- overflow: hidden;
181
- text-overflow: ellipsis;
182
  }
183
 
184
  .env-pill {
185
  display: inline-flex;
186
  align-items: center;
187
- gap: 5px;
188
- background: rgba(143, 136, 255, 0.1);
189
- border: 1px solid rgba(143, 136, 255, 0.2);
190
- padding: 3px 8px;
191
- border-radius: 6px;
192
- font-size: 0.65rem;
193
- font-weight: 600;
194
  text-transform: uppercase;
195
- letter-spacing: 0.06em;
196
- color: rgba(143, 136, 255, 0.9);
197
- font-family: 'Manrope', sans-serif;
198
- white-space: nowrap;
199
- overflow: hidden;
200
- text-overflow: ellipsis;
201
- max-width: 100%;
202
  }
203
 
204
  .nav {
@@ -209,199 +92,34 @@ body[data-theme='light'] {
209
 
210
  .nav-button {
211
  border: none;
212
- border-radius: 12px;
213
  padding: 12px 16px;
214
  display: flex;
215
  align-items: center;
216
  gap: 12px;
217
  background: transparent;
218
- color: #475569;
219
- font-weight: 600;
220
- font-family: 'Manrope', sans-serif;
221
- font-size: 0.875rem;
222
  cursor: pointer;
223
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
224
- position: relative;
225
- overflow: visible;
226
- }
227
-
228
- .nav-button svg {
229
- color: #6366f1;
230
- }
231
-
232
- .nav-button[data-nav="page-overview"] svg {
233
- color: #3b82f6;
234
- }
235
-
236
- .nav-button[data-nav="page-market"] svg {
237
- color: #8b5cf6;
238
- }
239
-
240
- .nav-button[data-nav="page-chart"] svg {
241
- color: #ec4899;
242
- }
243
-
244
- .nav-button[data-nav="page-ai"] svg {
245
- color: #10b981;
246
- }
247
-
248
- .nav-button[data-nav="page-news"] svg {
249
- color: #f59e0b;
250
- }
251
-
252
- .nav-button[data-nav="page-providers"] svg {
253
- color: #06b6d4;
254
- }
255
-
256
- .nav-button[data-nav="page-api"] svg {
257
- color: #6366f1;
258
- }
259
-
260
- .nav-button[data-nav="page-debug"] svg {
261
- color: #ef4444;
262
- }
263
-
264
- .nav-button[data-nav="page-datasets"] svg {
265
- color: #a855f7;
266
- }
267
-
268
- .nav-button[data-nav="page-settings"] svg {
269
- color: #64748b;
270
- }
271
-
272
- .nav-button::before {
273
- content: '';
274
- position: absolute;
275
- left: 0;
276
- top: 50%;
277
- transform: translateY(-50%);
278
- width: 2px;
279
- height: 0;
280
- background: var(--primary);
281
- border-radius: 0 2px 2px 0;
282
- transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
283
- opacity: 0;
284
- }
285
-
286
- .nav-button::after {
287
- content: '';
288
- position: absolute;
289
- inset: 0;
290
- background: rgba(143, 136, 255, 0.05);
291
- border-radius: 10px;
292
- opacity: 0;
293
- transition: opacity 0.25s ease;
294
- z-index: -1;
295
  }
296
 
297
  .nav-button svg {
298
- width: 20px;
299
- height: 20px;
300
  fill: currentColor;
301
- stroke: currentColor;
302
- stroke-width: 2;
303
- transition: all 0.25s ease;
304
- flex-shrink: 0;
305
- opacity: 1;
306
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
307
  }
308
 
 
309
  .nav-button:hover {
310
- color: #0f172a;
311
- background: rgba(99, 102, 241, 0.08);
312
- }
313
-
314
- .nav-button:hover svg {
315
- opacity: 1;
316
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
317
- }
318
-
319
- .nav-button:hover::before {
320
- height: 50%;
321
- opacity: 1;
322
- }
323
-
324
- .nav-button:hover::after {
325
- opacity: 1;
326
- }
327
-
328
- .nav-button:hover svg {
329
- opacity: 1;
330
- color: var(--primary);
331
- }
332
-
333
- .nav-button.active {
334
- background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.12));
335
- color: #0f172a;
336
- box-shadow:
337
- inset 0 1px 2px rgba(255, 255, 255, 0.3),
338
- 0 2px 8px rgba(99, 102, 241, 0.2);
339
- border: 1px solid rgba(99, 102, 241, 0.2);
340
- }
341
-
342
- .nav-button.active svg {
343
- opacity: 1;
344
- filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.3));
345
- }
346
-
347
- .nav-button.active::before {
348
- height: 60%;
349
- opacity: 1;
350
- box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
351
- }
352
-
353
- .nav-button.active::after {
354
- opacity: 1;
355
- background: rgba(143, 136, 255, 0.08);
356
- }
357
-
358
- .nav-button.active svg {
359
- color: var(--primary);
360
- opacity: 1;
361
  }
362
 
363
  .sidebar-footer {
364
  margin-top: auto;
365
- padding: 0;
366
- display: flex;
367
- align-items: center;
368
- justify-content: center;
369
- }
370
-
371
- .footer-badge {
372
- display: inline-flex;
373
- align-items: center;
374
- gap: 8px;
375
- padding: 10px 16px;
376
- background: rgba(255, 255, 255, 0.03);
377
- border: 1px solid rgba(255, 255, 255, 0.08);
378
- border-radius: 12px;
379
- font-size: 0.75rem;
380
- font-weight: 600;
381
  color: var(--text-muted);
382
- font-family: 'Manrope', sans-serif;
383
- letter-spacing: 0.05em;
384
- text-transform: uppercase;
385
- transition: all 0.3s ease;
386
- }
387
-
388
- .footer-badge svg {
389
- width: 14px;
390
- height: 14px;
391
- opacity: 0.7;
392
- transition: all 0.3s ease;
393
- }
394
-
395
- .footer-badge:hover {
396
- background: rgba(255, 255, 255, 0.06);
397
- border-color: rgba(143, 136, 255, 0.3);
398
- color: var(--text-primary);
399
- transform: translateY(-2px);
400
- }
401
-
402
- .footer-badge:hover svg {
403
- opacity: 1;
404
- color: var(--primary);
405
  }
406
 
407
  .main-area {
@@ -416,125 +134,19 @@ body[data-theme='light'] {
416
  display: flex;
417
  justify-content: space-between;
418
  align-items: center;
419
- padding: 24px 32px;
420
  border-radius: 24px;
421
- background: linear-gradient(135deg, rgba(20, 28, 45, 0.95), rgba(15, 23, 42, 0.9));
422
- border: 1px solid rgba(255, 255, 255, 0.2);
423
- box-shadow:
424
- var(--shadow-soft),
425
- 0 0 0 1px rgba(255, 255, 255, 0.08) inset,
426
- 0 0 40px rgba(143, 136, 255, 0.15),
427
- 0 4px 16px rgba(0, 0, 0, 0.3);
428
  backdrop-filter: blur(20px);
429
  flex-wrap: wrap;
430
- gap: 20px;
431
- position: relative;
432
- overflow: hidden;
433
- }
434
-
435
- .topbar::before {
436
- content: '';
437
- position: absolute;
438
- top: 0;
439
- left: 0;
440
- right: 0;
441
- height: 2px;
442
- background: linear-gradient(90deg, transparent, rgba(143, 136, 255, 0.5), rgba(22, 217, 250, 0.5), transparent);
443
- }
444
-
445
- .topbar-content {
446
- display: flex;
447
- align-items: center;
448
  gap: 16px;
449
- flex: 1;
450
- }
451
-
452
- .topbar-icon {
453
- display: flex;
454
- align-items: center;
455
- justify-content: center;
456
- width: 64px;
457
- height: 64px;
458
- border-radius: 14px;
459
- background: rgba(255, 255, 255, 0.04);
460
- border: 1px solid rgba(255, 255, 255, 0.08);
461
- color: var(--text-secondary);
462
- flex-shrink: 0;
463
- box-shadow:
464
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
465
- inset 0 -1px 2px rgba(0, 0, 0, 0.2),
466
- 0 2px 4px rgba(0, 0, 0, 0.2);
467
- position: relative;
468
- }
469
-
470
- .topbar-icon::before {
471
- content: '';
472
- position: absolute;
473
- top: 2px;
474
- left: 2px;
475
- right: 2px;
476
- height: 50%;
477
- border-radius: 14px 14px 0 0;
478
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent);
479
- pointer-events: none;
480
- }
481
-
482
- .topbar-icon svg {
483
- position: relative;
484
- z-index: 1;
485
- width: 36px;
486
- height: 36px;
487
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
488
- }
489
-
490
- .topbar-text {
491
- display: flex;
492
- flex-direction: column;
493
- gap: 6px;
494
  }
495
 
496
  .topbar h1 {
497
  margin: 0;
498
- font-size: 2rem;
499
- font-weight: 800;
500
- font-family: 'Manrope', 'DM Sans', sans-serif;
501
- letter-spacing: -0.03em;
502
- line-height: 1.2;
503
- display: flex;
504
- align-items: baseline;
505
- gap: 8px;
506
- }
507
-
508
- .title-gradient {
509
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 80%, #cbd5e1 100%);
510
- -webkit-background-clip: text;
511
- -webkit-text-fill-color: transparent;
512
- background-clip: text;
513
- text-shadow: 0 0 30px rgba(255, 255, 255, 0.3);
514
- }
515
-
516
- .title-accent {
517
- background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
518
- -webkit-background-clip: text;
519
- -webkit-text-fill-color: transparent;
520
- background-clip: text;
521
- font-size: 0.9em;
522
- }
523
-
524
- .topbar p.text-muted {
525
- margin: 0;
526
- font-size: 0.875rem;
527
- color: var(--text-muted);
528
- font-weight: 500;
529
- font-family: 'Manrope', sans-serif;
530
- display: flex;
531
- align-items: center;
532
- gap: 4px;
533
- }
534
-
535
- .topbar p.text-muted svg {
536
- opacity: 0.7;
537
- color: var(--primary);
538
  }
539
 
540
  .status-group {
@@ -546,247 +158,33 @@ body[data-theme='light'] {
546
  .status-pill {
547
  display: flex;
548
  align-items: center;
549
- gap: 12px;
550
- padding: 12px 20px;
551
- border-radius: 12px;
552
- background: rgba(255, 255, 255, 0.95);
553
- border: 2px solid rgba(0, 0, 0, 0.1);
554
- font-size: 0.8125rem;
555
- font-weight: 700;
556
  text-transform: uppercase;
557
- letter-spacing: 0.08em;
558
- font-family: 'Manrope', sans-serif;
559
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
560
- position: relative;
561
- overflow: visible;
562
- box-shadow:
563
- 0 2px 8px rgba(0, 0, 0, 0.1),
564
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
565
- cursor: pointer;
566
- }
567
-
568
- .status-pill:hover {
569
- box-shadow:
570
- 0 4px 12px rgba(0, 0, 0, 0.15),
571
- inset 0 1px 0 rgba(255, 255, 255, 0.9);
572
- border-color: rgba(0, 0, 0, 0.15);
573
- }
574
-
575
- .status-pill::before {
576
- content: '';
577
- position: absolute;
578
- inset: 0;
579
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
580
- opacity: 0;
581
- transition: opacity 0.3s ease;
582
- }
583
-
584
- .status-pill:hover::before {
585
- opacity: 1;
586
  }
587
 
588
  .status-dot {
589
- width: 16px;
590
- height: 16px;
591
  border-radius: 50%;
592
  background: var(--warning);
593
- position: relative;
594
- flex-shrink: 0;
595
- box-shadow:
596
- 0 0 8px var(--warning),
597
- 0 2px 4px rgba(0, 0, 0, 0.2);
598
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
599
- border: 2px solid rgba(255, 255, 255, 0.9);
600
- }
601
-
602
- .status-pill:hover .status-dot {
603
- box-shadow:
604
- 0 0 12px var(--warning),
605
- 0 2px 6px rgba(0, 0, 0, 0.25);
606
- }
607
-
608
- .status-pill[data-state='ok'] {
609
- background: linear-gradient(135deg, #10b981 0%, #059669 100%);
610
- border: 2px solid #10b981;
611
- color: #ffffff;
612
- box-shadow:
613
- 0 2px 8px rgba(16, 185, 129, 0.3),
614
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
615
- font-weight: 700;
616
- position: relative;
617
- }
618
-
619
- .status-pill[data-state='ok']:hover {
620
- background: linear-gradient(135deg, #059669 0%, #047857 100%);
621
- box-shadow:
622
- 0 4px 12px rgba(16, 185, 129, 0.4),
623
- inset 0 1px 0 rgba(255, 255, 255, 0.4);
624
- }
625
-
626
- @keyframes live-pulse {
627
- 0%, 100% {
628
- box-shadow:
629
- inset 0 1px 2px rgba(255, 255, 255, 0.2),
630
- inset 0 -1px 2px rgba(0, 0, 0, 0.3),
631
- 0 4px 16px rgba(34, 197, 94, 0.4),
632
- 0 0 30px rgba(34, 197, 94, 0.3),
633
- 0 0 50px rgba(16, 185, 129, 0.2);
634
- }
635
- 50% {
636
- box-shadow:
637
- inset 0 1px 2px rgba(255, 255, 255, 0.25),
638
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
639
- 0 6px 24px rgba(34, 197, 94, 0.5),
640
- 0 0 40px rgba(34, 197, 94, 0.4),
641
- 0 0 60px rgba(16, 185, 129, 0.3);
642
- }
643
- }
644
-
645
- .status-pill[data-state='ok']::before {
646
- content: '';
647
- position: absolute;
648
- top: 2px;
649
- left: 2px;
650
- right: 2px;
651
- height: 50%;
652
- border-radius: 999px 999px 0 0;
653
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
654
- pointer-events: none;
655
  }
656
 
657
  .status-pill[data-state='ok'] .status-dot {
658
- background: #ffffff;
659
- border: 2px solid #10b981;
660
- box-shadow:
661
- 0 0 8px rgba(16, 185, 129, 0.6),
662
- 0 2px 4px rgba(0, 0, 0, 0.2),
663
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
664
- }
665
-
666
- .status-pill[data-state='ok']:hover .status-dot {
667
- box-shadow:
668
- 0 0 12px rgba(16, 185, 129, 0.8),
669
- 0 2px 6px rgba(0, 0, 0, 0.25),
670
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
671
- }
672
-
673
- @keyframes live-dot-pulse {
674
- 0%, 100% {
675
- transform: scale(1);
676
- box-shadow:
677
- inset 0 1px 2px rgba(255, 255, 255, 0.4),
678
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
679
- 0 0 16px rgba(34, 197, 94, 0.8),
680
- 0 0 32px rgba(34, 197, 94, 0.6),
681
- 0 0 48px rgba(16, 185, 129, 0.4);
682
- }
683
- 50% {
684
- transform: scale(1.15);
685
- box-shadow:
686
- inset 0 1px 2px rgba(255, 255, 255, 0.5),
687
- inset 0 -1px 2px rgba(0, 0, 0, 0.5),
688
- 0 0 20px rgba(34, 197, 94, 1),
689
- 0 0 40px rgba(34, 197, 94, 0.8),
690
- 0 0 60px rgba(16, 185, 129, 0.6);
691
- }
692
- }
693
-
694
- .status-pill[data-state='ok']::after {
695
- display: none;
696
- }
697
-
698
- .status-pill[data-state='warn'] {
699
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
700
- border: 2px solid #f59e0b;
701
- color: #ffffff;
702
- box-shadow:
703
- 0 2px 8px rgba(245, 158, 11, 0.3),
704
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
705
- }
706
-
707
- .status-pill[data-state='warn']:hover {
708
- background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
709
- box-shadow:
710
- 0 4px 12px rgba(245, 158, 11, 0.4),
711
- inset 0 1px 0 rgba(255, 255, 255, 0.4);
712
  }
713
 
714
  .status-pill[data-state='warn'] .status-dot {
715
- background: #ffffff;
716
- border: 2px solid #f59e0b;
717
- box-shadow:
718
- 0 0 8px rgba(245, 158, 11, 0.6),
719
- 0 2px 4px rgba(0, 0, 0, 0.2),
720
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
721
- }
722
-
723
- .status-pill[data-state='warn']:hover .status-dot {
724
- box-shadow:
725
- 0 0 12px rgba(245, 158, 11, 0.8),
726
- 0 2px 6px rgba(0, 0, 0, 0.25),
727
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
728
- }
729
-
730
- .status-pill[data-state='error'] {
731
- background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
732
- border: 2px solid #ef4444;
733
- color: #ffffff;
734
- box-shadow:
735
- 0 2px 8px rgba(239, 68, 68, 0.3),
736
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
737
- }
738
-
739
- .status-pill[data-state='error']:hover {
740
- background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
741
- box-shadow:
742
- 0 4px 12px rgba(239, 68, 68, 0.4),
743
- inset 0 1px 0 rgba(255, 255, 255, 0.4);
744
  }
745
 
746
  .status-pill[data-state='error'] .status-dot {
747
- background: #ffffff;
748
- border: 2px solid #ef4444;
749
- box-shadow:
750
- 0 0 8px rgba(239, 68, 68, 0.6),
751
- 0 2px 4px rgba(0, 0, 0, 0.2),
752
- inset 0 1px 2px rgba(255, 255, 255, 0.8);
753
- }
754
-
755
- .status-pill[data-state='error']:hover .status-dot {
756
- box-shadow:
757
- 0 0 12px rgba(239, 68, 68, 0.8),
758
- 0 2px 6px rgba(0, 0, 0, 0.25),
759
- inset 0 1px 2px rgba(255, 255, 255, 0.9);
760
- }
761
-
762
- @keyframes pulse-green {
763
- 0%, 100% {
764
- transform: scale(1);
765
- opacity: 1;
766
- box-shadow:
767
- 0 0 16px #86efac,
768
- 0 0 32px rgba(74, 222, 128, 0.8),
769
- 0 0 48px rgba(34, 197, 94, 0.6);
770
- }
771
- 50% {
772
- transform: scale(1.3);
773
- opacity: 0.9;
774
- box-shadow:
775
- 0 0 24px #86efac,
776
- 0 0 48px rgba(74, 222, 128, 1),
777
- 0 0 72px rgba(34, 197, 94, 0.8);
778
- }
779
- }
780
-
781
- @keyframes glow-pulse {
782
- 0%, 100% {
783
- opacity: 0.6;
784
- transform: scale(1);
785
- }
786
- 50% {
787
- opacity: 1;
788
- transform: scale(1.1);
789
- }
790
  }
791
 
792
  .page-container {
@@ -810,117 +208,18 @@ body[data-theme='light'] {
810
  }
811
 
812
  .section-title {
813
- font-size: 1.75rem;
814
- font-weight: 800;
815
- letter-spacing: -0.025em;
816
- font-family: 'Manrope', 'DM Sans', sans-serif;
817
- margin: 0;
818
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
819
- -webkit-background-clip: text;
820
- -webkit-text-fill-color: transparent;
821
- background-clip: text;
822
  }
823
 
824
  .glass-card {
825
- background: rgba(255, 255, 255, 0.12);
826
- backdrop-filter: blur(35px) saturate(180%);
827
- -webkit-backdrop-filter: blur(35px) saturate(180%);
828
- border: 2px solid rgba(255, 255, 255, 0.22);
829
  border-radius: 24px;
830
- padding: 28px;
831
- box-shadow:
832
- 0 8px 32px rgba(0, 0, 0, 0.5),
833
- inset 0 1px 0 rgba(255, 255, 255, 0.25),
834
- inset 0 -1px 0 rgba(0, 0, 0, 0.3),
835
- 0 0 40px rgba(0, 212, 255, 0.15);
836
  position: relative;
837
- overflow: visible;
838
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
839
- }
840
-
841
- .glass-card::before {
842
- content: '';
843
- position: absolute;
844
- inset: -4px;
845
- background: linear-gradient(135deg,
846
- rgba(0, 212, 255, 0.3) 0%,
847
- rgba(139, 92, 246, 0.3) 50%,
848
- rgba(236, 72, 153, 0.3) 100%);
849
- border-radius: 28px;
850
- opacity: 0;
851
- transition: opacity 0.4s ease;
852
- z-index: -1;
853
- filter: blur(20px);
854
- animation: card-glow-pulse 4s ease-in-out infinite;
855
- }
856
-
857
- @keyframes card-glow-pulse {
858
- 0%, 100% {
859
- opacity: 0;
860
- filter: blur(20px);
861
- }
862
- 50% {
863
- opacity: 0.4;
864
- filter: blur(25px);
865
- }
866
- }
867
-
868
- .glass-card::after {
869
- content: '';
870
- position: absolute;
871
- top: 0;
872
- left: 0;
873
- right: 0;
874
- height: 3px;
875
- background: linear-gradient(90deg,
876
- transparent,
877
- rgba(0, 212, 255, 0.7),
878
- rgba(139, 92, 246, 0.7),
879
- rgba(236, 72, 153, 0.7),
880
- transparent);
881
- border-radius: 24px 24px 0 0;
882
- opacity: 0.7;
883
- animation: card-shimmer 4s infinite;
884
- }
885
-
886
- @keyframes card-shimmer {
887
- 0%, 100% { opacity: 0.7; }
888
- 50% { opacity: 1; }
889
- }
890
-
891
-
892
- .glass-card:hover {
893
- box-shadow:
894
- 0 16px 48px rgba(0, 0, 0, 0.6),
895
- 0 0 50px rgba(0, 212, 255, 0.5),
896
- 0 0 100px rgba(139, 92, 246, 0.4),
897
- 0 0 150px rgba(236, 72, 153, 0.3),
898
- inset 0 1px 0 rgba(255, 255, 255, 0.3),
899
- inset 0 -1px 0 rgba(0, 0, 0, 0.4);
900
- border-color: rgba(0, 212, 255, 0.6);
901
- }
902
-
903
- .glass-card:hover::before {
904
- opacity: 0.8;
905
- filter: blur(30px);
906
- }
907
-
908
- .glass-card:hover::after {
909
- opacity: 1;
910
- height: 4px;
911
- }
912
-
913
- .glass-card h4 {
914
- font-size: 1.25rem;
915
- font-weight: 700;
916
- font-family: 'Manrope', 'DM Sans', sans-serif;
917
- margin: 0 0 20px 0;
918
- color: var(--text-primary);
919
- letter-spacing: -0.02em;
920
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
921
- -webkit-background-clip: text;
922
- -webkit-text-fill-color: transparent;
923
- background-clip: text;
924
  }
925
 
926
  .glass-card::before {
@@ -943,379 +242,24 @@ body[data-theme='light'] {
943
  margin-bottom: 24px;
944
  }
945
 
946
- .stat-card {
947
- display: flex;
948
- flex-direction: column;
949
- gap: 1rem;
950
- position: relative;
951
- background: rgba(255, 255, 255, 0.08);
952
- backdrop-filter: blur(30px) saturate(180%);
953
- -webkit-backdrop-filter: blur(30px) saturate(180%);
954
- border: 2px solid rgba(255, 255, 255, 0.15);
955
- border-radius: 20px;
956
- padding: 20px;
957
- box-shadow:
958
- 0 8px 32px rgba(0, 0, 0, 0.4),
959
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
960
- inset 0 -1px 0 rgba(0, 0, 0, 0.2);
961
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
962
- overflow: visible;
963
- }
964
-
965
- .stat-card::before {
966
- content: '';
967
- position: absolute;
968
- inset: -4px;
969
- border-radius: 24px;
970
- background: linear-gradient(135deg,
971
- rgba(0, 212, 255, 0.4) 0%,
972
- rgba(139, 92, 246, 0.4) 50%,
973
- rgba(236, 72, 153, 0.4) 100%);
974
- opacity: 0;
975
- transition: opacity 0.4s ease;
976
- z-index: -1;
977
- filter: blur(20px);
978
- animation: glow-pulse 3s ease-in-out infinite;
979
- }
980
-
981
- .stat-card::after {
982
- content: '';
983
- position: absolute;
984
- top: 0;
985
- left: 0;
986
- right: 0;
987
- height: 3px;
988
- background: linear-gradient(90deg,
989
- transparent,
990
- rgba(0, 212, 255, 0.8),
991
- rgba(139, 92, 246, 0.8),
992
- rgba(236, 72, 153, 0.8),
993
- transparent);
994
- border-radius: 20px 20px 0 0;
995
- opacity: 0.6;
996
- animation: shimmer 3s infinite;
997
- }
998
-
999
- @keyframes glow-pulse {
1000
- 0%, 100% {
1001
- opacity: 0;
1002
- filter: blur(20px);
1003
- }
1004
- 50% {
1005
- opacity: 0.5;
1006
- filter: blur(25px);
1007
- }
1008
- }
1009
-
1010
- @keyframes shimmer {
1011
- 0%, 100% { opacity: 0.6; }
1012
- 50% { opacity: 1; }
1013
- }
1014
-
1015
- .stat-card:hover {
1016
- border-color: rgba(0, 212, 255, 0.5);
1017
- box-shadow:
1018
- 0 16px 48px rgba(0, 0, 0, 0.5),
1019
- 0 0 40px rgba(0, 212, 255, 0.4),
1020
- 0 0 80px rgba(139, 92, 246, 0.3),
1021
- inset 0 1px 0 rgba(255, 255, 255, 0.3),
1022
- inset 0 -1px 0 rgba(0, 0, 0, 0.3);
1023
- }
1024
-
1025
- .stat-card:hover::before {
1026
- opacity: 0.6;
1027
- filter: blur(25px);
1028
- }
1029
-
1030
- .stat-card:hover::after {
1031
- opacity: 1;
1032
- height: 4px;
1033
- }
1034
-
1035
- .stat-header {
1036
- display: flex;
1037
- align-items: center;
1038
- gap: 0.75rem;
1039
- }
1040
-
1041
- .stat-icon {
1042
- display: flex;
1043
- align-items: center;
1044
- justify-content: center;
1045
- width: 52px;
1046
- height: 52px;
1047
- border-radius: 14px;
1048
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(139, 92, 246, 0.2));
1049
- flex-shrink: 0;
1050
- border: 2px solid rgba(0, 212, 255, 0.3);
1051
- box-shadow:
1052
- inset 0 1px 2px rgba(255, 255, 255, 0.2),
1053
- inset 0 -1px 2px rgba(0, 0, 0, 0.3),
1054
- 0 4px 12px rgba(0, 212, 255, 0.3),
1055
- 0 0 20px rgba(0, 212, 255, 0.2);
1056
- transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1057
- position: relative;
1058
- color: #00D4FF;
1059
- overflow: visible;
1060
- }
1061
-
1062
- .stat-icon::after {
1063
- content: '';
1064
- position: absolute;
1065
- inset: -2px;
1066
- border-radius: 16px;
1067
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.4), rgba(139, 92, 246, 0.4));
1068
- opacity: 0;
1069
- filter: blur(12px);
1070
- transition: opacity 0.4s ease;
1071
- z-index: -1;
1072
- }
1073
-
1074
- .stat-icon::before {
1075
- content: '';
1076
- position: absolute;
1077
- top: 2px;
1078
- left: 2px;
1079
- right: 2px;
1080
- height: 50%;
1081
- border-radius: 12px 12px 0 0;
1082
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.15), transparent);
1083
- pointer-events: none;
1084
- }
1085
-
1086
- .stat-icon svg {
1087
- position: relative;
1088
- z-index: 1;
1089
- width: 22px;
1090
- height: 22px;
1091
- opacity: 0.85;
1092
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
1093
- }
1094
-
1095
- .stat-card:hover .stat-icon {
1096
- box-shadow:
1097
- inset 0 1px 2px rgba(255, 255, 255, 0.25),
1098
- inset 0 -1px 2px rgba(0, 0, 0, 0.4),
1099
- 0 8px 24px rgba(0, 212, 255, 0.5),
1100
- 0 0 40px rgba(0, 212, 255, 0.4),
1101
- 0 0 60px rgba(139, 92, 246, 0.3);
1102
- border-color: rgba(0, 212, 255, 0.6);
1103
- background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), rgba(139, 92, 246, 0.3));
1104
- }
1105
-
1106
- .stat-card:hover .stat-icon::after {
1107
- opacity: 0.8;
1108
- filter: blur(16px);
1109
- }
1110
-
1111
- .stat-card:hover .stat-icon svg {
1112
- opacity: 1;
1113
- }
1114
-
1115
  .stat-card h3 {
1116
- font-size: 0.8125rem;
1117
- font-weight: 700;
1118
  text-transform: uppercase;
1119
- letter-spacing: 0.1em;
1120
- color: rgba(255, 255, 255, 0.7);
1121
- margin: 0;
1122
- font-family: 'Manrope', 'DM Sans', sans-serif;
1123
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
1124
  }
1125
 
1126
  .stat-value {
1127
- font-size: 2rem;
1128
- font-weight: 800;
1129
- margin: 0;
1130
- font-family: 'Manrope', 'DM Sans', sans-serif;
1131
- letter-spacing: -0.03em;
1132
- line-height: 1.1;
1133
- background: linear-gradient(135deg, #ffffff 0%, #00D4FF 50%, #8B5CF6 100%);
1134
- -webkit-background-clip: text;
1135
- -webkit-text-fill-color: transparent;
1136
- background-clip: text;
1137
- position: relative;
1138
- filter: drop-shadow(0 0 8px rgba(0, 212, 255, 0.4));
1139
- text-shadow:
1140
- 0 2px 12px rgba(0, 0, 0, 0.5),
1141
- 0 0 30px rgba(0, 212, 255, 0.3);
1142
- }
1143
-
1144
- .stat-card:hover .stat-value {
1145
- filter: drop-shadow(0 0 12px rgba(0, 212, 255, 0.6)) drop-shadow(0 0 24px rgba(139, 92, 246, 0.4));
1146
- background: linear-gradient(135deg, #ffffff 0%, #00D4FF 30%, #8B5CF6 70%, #EC4899 100%);
1147
- -webkit-background-clip: text;
1148
- -webkit-text-fill-color: transparent;
1149
- background-clip: text;
1150
- }
1151
-
1152
- .stat-value-wrapper {
1153
- display: flex;
1154
- flex-direction: column;
1155
- gap: 0.5rem;
1156
- margin: 0.5rem 0;
1157
- }
1158
-
1159
- .stat-change {
1160
- display: inline-flex;
1161
- align-items: center;
1162
- gap: 0.375rem;
1163
- font-size: 0.8125rem;
1164
- font-weight: 600;
1165
- font-family: 'Manrope', sans-serif;
1166
- width: fit-content;
1167
- transition: all 0.2s ease;
1168
- }
1169
-
1170
- .change-icon-wrapper {
1171
- display: flex;
1172
- align-items: center;
1173
- justify-content: center;
1174
- width: 16px;
1175
- height: 16px;
1176
- flex-shrink: 0;
1177
- opacity: 0.8;
1178
- }
1179
-
1180
- .change-icon-wrapper.positive {
1181
- color: #22c55e;
1182
- }
1183
-
1184
- .change-icon-wrapper.negative {
1185
- color: #ef4444;
1186
- }
1187
-
1188
- .stat-change.positive {
1189
- color: #4ade80;
1190
- background: rgba(34, 197, 94, 0.2);
1191
- padding: 4px 10px;
1192
- border-radius: 8px;
1193
- border: 1px solid rgba(34, 197, 94, 0.4);
1194
- box-shadow:
1195
- 0 2px 8px rgba(34, 197, 94, 0.3),
1196
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
1197
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1198
- font-weight: 700;
1199
- }
1200
-
1201
- .stat-change.negative {
1202
- color: #f87171;
1203
- background: rgba(239, 68, 68, 0.2);
1204
- padding: 4px 10px;
1205
- border-radius: 8px;
1206
- border: 1px solid rgba(239, 68, 68, 0.4);
1207
- box-shadow:
1208
- 0 2px 8px rgba(239, 68, 68, 0.3),
1209
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
1210
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1211
- font-weight: 700;
1212
- }
1213
-
1214
- .change-value {
1215
  font-weight: 600;
1216
- letter-spacing: 0.01em;
1217
- }
1218
-
1219
- .stat-metrics {
1220
- display: flex;
1221
- gap: 1rem;
1222
- margin-top: auto;
1223
- padding-top: 1rem;
1224
- border-top: 2px solid rgba(255, 255, 255, 0.12);
1225
- background: linear-gradient(90deg,
1226
- transparent,
1227
- rgba(0, 212, 255, 0.05),
1228
- rgba(139, 92, 246, 0.05),
1229
- transparent);
1230
- margin-left: -20px;
1231
- margin-right: -20px;
1232
- padding-left: 20px;
1233
- padding-right: 20px;
1234
- border-radius: 0 0 20px 20px;
1235
- }
1236
-
1237
- .stat-metric {
1238
- display: flex;
1239
- flex-direction: column;
1240
- gap: 0.25rem;
1241
- flex: 1;
1242
- }
1243
-
1244
- .stat-metric .metric-label {
1245
- font-size: 0.7rem;
1246
- text-transform: uppercase;
1247
- letter-spacing: 0.1em;
1248
- color: rgba(255, 255, 255, 0.6);
1249
- font-weight: 700;
1250
- font-family: 'Manrope', sans-serif;
1251
- text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
1252
- }
1253
-
1254
- .stat-metric .metric-value {
1255
- font-size: 0.9375rem;
1256
- font-weight: 700;
1257
- font-family: 'Manrope', 'DM Sans', sans-serif;
1258
- color: rgba(255, 255, 255, 0.9);
1259
- display: flex;
1260
- align-items: center;
1261
- gap: 6px;
1262
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
1263
- }
1264
-
1265
- .metric-icon {
1266
- display: inline-flex;
1267
- align-items: center;
1268
- justify-content: center;
1269
- width: 18px;
1270
- height: 18px;
1271
- border-radius: 4px;
1272
- font-size: 0.75rem;
1273
- font-weight: 700;
1274
- flex-shrink: 0;
1275
- }
1276
-
1277
- .metric-icon.positive {
1278
- background: rgba(34, 197, 94, 0.3);
1279
- color: #4ade80;
1280
- border: 1px solid rgba(34, 197, 94, 0.5);
1281
- box-shadow:
1282
- 0 2px 8px rgba(34, 197, 94, 0.4),
1283
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
1284
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1285
- }
1286
-
1287
- .metric-icon.negative {
1288
- background: rgba(239, 68, 68, 0.3);
1289
- color: #f87171;
1290
- border: 1px solid rgba(239, 68, 68, 0.5);
1291
- box-shadow:
1292
- 0 2px 8px rgba(239, 68, 68, 0.4),
1293
- inset 0 1px 0 rgba(255, 255, 255, 0.2);
1294
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1295
- }
1296
-
1297
- .stat-metric .metric-value.positive {
1298
- color: #4ade80;
1299
- text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
1300
- font-weight: 800;
1301
- }
1302
-
1303
- .stat-metric .metric-value.negative {
1304
- color: #f87171;
1305
- text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
1306
- font-weight: 800;
1307
  }
1308
 
1309
  .stat-trend {
1310
  display: flex;
1311
  align-items: center;
1312
  gap: 6px;
1313
- font-size: 0.8125rem;
1314
- color: var(--text-faint);
1315
- font-family: 'Manrope', sans-serif;
1316
- font-weight: 500;
1317
- margin-top: auto;
1318
- letter-spacing: 0.02em;
1319
  }
1320
 
1321
  .grid-two {
@@ -1324,153 +268,35 @@ body[data-theme='light'] {
1324
  gap: 20px;
1325
  }
1326
 
1327
- .grid-three {
1328
- display: grid;
1329
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1330
- gap: 18px;
1331
- }
1332
-
1333
- .grid-four {
1334
- display: grid;
1335
- grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1336
- gap: 18px;
1337
- }
1338
-
1339
  .table-wrapper {
1340
  overflow: auto;
1341
  }
1342
 
1343
  table {
1344
  width: 100%;
1345
- border-collapse: separate;
1346
- border-spacing: 0;
1347
  }
1348
 
1349
  th, td {
1350
  text-align: left;
1351
- padding: 12px 14px;
1352
- font-size: 0.8125rem;
1353
- font-family: 'Manrope', 'DM Sans', sans-serif;
1354
  }
1355
 
1356
  th {
1357
- font-size: 0.7rem;
1358
- font-weight: 700;
1359
- letter-spacing: 0.06em;
1360
  color: var(--text-muted);
1361
  text-transform: uppercase;
1362
- border-bottom: 2px solid rgba(255, 255, 255, 0.1);
1363
- background: rgba(255, 255, 255, 0.03);
1364
- position: sticky;
1365
- top: 0;
1366
- z-index: 10;
1367
- white-space: nowrap;
1368
- }
1369
-
1370
- th:first-child {
1371
- border-top-left-radius: 12px;
1372
- padding-left: 16px;
1373
- }
1374
-
1375
- th:last-child {
1376
- border-top-right-radius: 12px;
1377
- padding-right: 16px;
1378
- }
1379
-
1380
- td {
1381
- font-weight: 500;
1382
- color: var(--text-primary);
1383
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1384
- vertical-align: middle;
1385
- }
1386
-
1387
- td:first-child {
1388
- padding-left: 16px;
1389
- font-weight: 600;
1390
- color: var(--text-muted);
1391
- font-size: 0.75rem;
1392
- }
1393
-
1394
- td:last-child {
1395
- padding-right: 16px;
1396
  }
1397
 
1398
  tr {
1399
- transition: all 0.2s ease;
1400
- }
1401
-
1402
- tbody tr {
1403
- border-left: 2px solid transparent;
1404
- transition: all 0.2s ease;
1405
  }
1406
 
1407
  tbody tr:hover {
1408
- background: rgba(255, 255, 255, 0.05);
1409
- border-left-color: rgba(143, 136, 255, 0.4);
1410
- transform: translateX(2px);
1411
- }
1412
-
1413
- tbody tr:last-child td:first-child {
1414
- border-bottom-left-radius: 12px;
1415
- }
1416
-
1417
- tbody tr:last-child td:last-child {
1418
- border-bottom-right-radius: 12px;
1419
- }
1420
-
1421
- tbody tr:last-child td {
1422
- border-bottom: none;
1423
- }
1424
-
1425
- td.text-success,
1426
- td.text-danger {
1427
- display: flex;
1428
- align-items: center;
1429
- gap: 6px;
1430
- font-weight: 600;
1431
- font-size: 0.8125rem;
1432
- }
1433
-
1434
- td.text-success {
1435
- color: #22c55e;
1436
- }
1437
-
1438
- td.text-danger {
1439
- color: #ef4444;
1440
- }
1441
-
1442
- .table-change-icon {
1443
- display: inline-flex;
1444
- align-items: center;
1445
- justify-content: center;
1446
- width: 16px;
1447
- height: 16px;
1448
- flex-shrink: 0;
1449
- opacity: 0.9;
1450
- }
1451
-
1452
- .table-change-icon.positive {
1453
- color: #22c55e;
1454
- }
1455
-
1456
- .table-change-icon.negative {
1457
- color: #ef4444;
1458
- }
1459
-
1460
- /* Chip styling for symbol column */
1461
- .chip {
1462
- display: inline-flex;
1463
- align-items: center;
1464
- padding: 4px 10px;
1465
- background: rgba(143, 136, 255, 0.1);
1466
- border: 1px solid rgba(143, 136, 255, 0.2);
1467
- border-radius: 6px;
1468
- font-size: 0.75rem;
1469
- font-weight: 600;
1470
- color: var(--primary-light);
1471
- font-family: 'Manrope', sans-serif;
1472
- letter-spacing: 0.02em;
1473
- text-transform: uppercase;
1474
  }
1475
 
1476
  .badge {
@@ -1541,275 +367,51 @@ td.text-danger {
1541
 
1542
  .input-chip {
1543
  border: 1px solid var(--glass-border);
1544
- background: rgba(255, 255, 255, 0.05);
1545
  border-radius: 999px;
1546
  padding: 8px 14px;
1547
  color: var(--text-muted);
1548
  display: inline-flex;
1549
  align-items: center;
1550
  gap: 10px;
1551
- font-family: 'Inter', sans-serif;
1552
- font-size: 0.875rem;
1553
  }
1554
 
1555
- .search-bar {
1556
- display: flex;
1557
- flex-wrap: wrap;
1558
- gap: 12px;
1559
- align-items: center;
1560
- margin-bottom: 20px;
1561
- padding: 16px;
1562
- background: var(--glass-bg);
1563
  border: 1px solid var(--glass-border);
1564
- border-radius: 16px;
1565
- backdrop-filter: blur(10px);
 
 
1566
  }
1567
 
1568
- .button-group {
1569
- display: flex;
1570
- gap: 8px;
1571
- flex-wrap: wrap;
1572
- }
1573
-
1574
- input[type='text'], select, textarea {
1575
- width: 100%;
1576
- background: rgba(255, 255, 255, 0.05);
1577
- border: 1px solid var(--glass-border);
1578
- border-radius: 12px;
1579
- padding: 12px 16px;
1580
- color: var(--text-primary);
1581
- font-family: 'Inter', sans-serif;
1582
- font-size: 0.9375rem;
1583
- transition: all 0.2s ease;
1584
- }
1585
-
1586
- input[type='text']:focus, select:focus, textarea:focus {
1587
- outline: none;
1588
- border-color: var(--primary);
1589
- background: rgba(255, 255, 255, 0.08);
1590
- box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
1591
- }
1592
-
1593
- textarea {
1594
- min-height: 100px;
1595
  }
1596
 
1597
  button.primary {
1598
  background: linear-gradient(120deg, var(--primary), var(--secondary));
1599
  border: none;
1600
- border-radius: 10px;
1601
  color: #fff;
1602
- padding: 10px 14px;
1603
- font-weight: 500;
1604
- font-family: 'Manrope', sans-serif;
1605
- font-size: 0.875rem;
1606
  cursor: pointer;
1607
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1608
- box-shadow:
1609
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
1610
- 0 2px 8px rgba(143, 136, 255, 0.2);
1611
- position: relative;
1612
- overflow: visible;
1613
- display: flex;
1614
- align-items: center;
1615
- gap: 10px;
1616
- }
1617
-
1618
- button.primary::before {
1619
- content: '';
1620
- position: absolute;
1621
- top: 0;
1622
- left: 0;
1623
- right: 0;
1624
- height: 50%;
1625
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
1626
- border-radius: 12px 12px 0 0;
1627
- pointer-events: none;
1628
  }
1629
 
1630
  button.primary:hover {
1631
- background: linear-gradient(135deg, #2563eb 0%, #4f46e5 50%, #7c3aed 100%);
1632
- box-shadow:
1633
- 0 6px 20px rgba(59, 130, 246, 0.5),
1634
- inset 0 1px 0 rgba(255, 255, 255, 0.4),
1635
- inset 0 -1px 0 rgba(0, 0, 0, 0.15);
1636
- }
1637
-
1638
- button.primary:hover::before {
1639
- height: 50%;
1640
- opacity: 1;
1641
- }
1642
-
1643
- button.primary:active {
1644
- box-shadow:
1645
- 0 2px 8px rgba(59, 130, 246, 0.4),
1646
- inset 0 2px 4px rgba(0, 0, 0, 0.2);
1647
- }
1648
-
1649
- button.secondary {
1650
- background: rgba(255, 255, 255, 0.95);
1651
- border: 2px solid #3b82f6;
1652
- border-radius: 12px;
1653
- color: #3b82f6;
1654
- padding: 14px 28px;
1655
- font-weight: 700;
1656
- font-family: 'Manrope', sans-serif;
1657
- font-size: 0.875rem;
1658
- cursor: pointer;
1659
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1660
- position: relative;
1661
- overflow: hidden;
1662
- display: flex;
1663
- align-items: center;
1664
- gap: 10px;
1665
- box-shadow:
1666
- 0 2px 8px rgba(59, 130, 246, 0.2),
1667
- inset 0 1px 0 rgba(255, 255, 255, 0.8);
1668
- }
1669
-
1670
- button.secondary::before {
1671
- content: '';
1672
- position: absolute;
1673
- top: 0;
1674
- left: 0;
1675
- right: 0;
1676
- height: 50%;
1677
- background: linear-gradient(180deg, rgba(59, 130, 246, 0.1), transparent);
1678
- border-radius: 12px 12px 0 0;
1679
- pointer-events: none;
1680
- }
1681
-
1682
- button.secondary::before {
1683
- content: '';
1684
- position: absolute;
1685
- left: 0;
1686
- top: 50%;
1687
- transform: translateY(-50%);
1688
- width: 2px;
1689
- height: 0;
1690
- background: var(--primary);
1691
- border-radius: 0 2px 2px 0;
1692
- transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1693
- opacity: 0;
1694
- }
1695
-
1696
- button.secondary::after {
1697
- content: '';
1698
- position: absolute;
1699
- inset: 0;
1700
- background: rgba(143, 136, 255, 0.05);
1701
- border-radius: 10px;
1702
- opacity: 0;
1703
- transition: opacity 0.25s ease;
1704
- z-index: -1;
1705
- }
1706
-
1707
- button.secondary:hover {
1708
- background: #3b82f6;
1709
- color: #ffffff;
1710
- box-shadow:
1711
- 0 4px 16px rgba(59, 130, 246, 0.4),
1712
- inset 0 1px 0 rgba(255, 255, 255, 0.3);
1713
- }
1714
-
1715
- button.secondary:hover::before {
1716
- height: 50%;
1717
- opacity: 1;
1718
- }
1719
-
1720
- button.secondary:hover::after {
1721
- opacity: 1;
1722
- }
1723
-
1724
- button.secondary.active {
1725
- background: rgba(143, 136, 255, 0.12);
1726
- border-color: rgba(143, 136, 255, 0.2);
1727
- color: var(--text-primary);
1728
- font-weight: 600;
1729
- box-shadow:
1730
- inset 0 1px 2px rgba(255, 255, 255, 0.1),
1731
- 0 2px 8px rgba(143, 136, 255, 0.2);
1732
- }
1733
-
1734
- button.secondary.active::before {
1735
- height: 60%;
1736
- opacity: 1;
1737
- box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
1738
  }
1739
 
1740
  button.ghost {
1741
- background: rgba(255, 255, 255, 0.9);
1742
- border: 1px solid rgba(0, 0, 0, 0.1);
1743
- border-radius: 10px;
1744
- padding: 10px 16px;
1745
- color: #475569;
1746
- font-weight: 600;
1747
- font-family: 'Manrope', sans-serif;
1748
- font-size: 0.875rem;
1749
  cursor: pointer;
1750
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1751
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
1752
- position: relative;
1753
- overflow: visible;
1754
- display: flex;
1755
- align-items: center;
1756
- gap: 10px;
1757
- }
1758
-
1759
- button.ghost::before {
1760
- content: '';
1761
- position: absolute;
1762
- left: 0;
1763
- top: 50%;
1764
- transform: translateY(-50%);
1765
- width: 2px;
1766
- height: 0;
1767
- background: var(--primary);
1768
- border-radius: 0 2px 2px 0;
1769
- transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1770
- opacity: 0;
1771
- }
1772
-
1773
- button.ghost::after {
1774
- content: '';
1775
- position: absolute;
1776
- inset: 0;
1777
- background: rgba(143, 136, 255, 0.05);
1778
- border-radius: 10px;
1779
- opacity: 0;
1780
- transition: opacity 0.25s ease;
1781
- z-index: -1;
1782
- }
1783
-
1784
- button.ghost:hover {
1785
- background: rgba(255, 255, 255, 1);
1786
- border-color: rgba(59, 130, 246, 0.3);
1787
- color: #3b82f6;
1788
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
1789
- }
1790
-
1791
- button.ghost:hover::before {
1792
- height: 50%;
1793
- opacity: 1;
1794
- }
1795
-
1796
- button.ghost:hover::after {
1797
- opacity: 1;
1798
- }
1799
-
1800
- button.ghost.active {
1801
- background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(99, 102, 241, 0.12));
1802
- border-color: rgba(59, 130, 246, 0.4);
1803
- color: #3b82f6;
1804
- box-shadow:
1805
- inset 0 1px 2px rgba(255, 255, 255, 0.3),
1806
- 0 2px 8px rgba(59, 130, 246, 0.3);
1807
- }
1808
-
1809
- button.ghost.active::before {
1810
- height: 60%;
1811
- opacity: 1;
1812
- box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
1813
  }
1814
 
1815
  .skeleton {
@@ -1900,107 +502,6 @@ button.ghost.active::before {
1900
  font-size: 0.75rem;
1901
  }
1902
 
1903
- .backend-info-grid {
1904
- display: grid;
1905
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1906
- gap: 16px;
1907
- }
1908
-
1909
- .backend-info-item {
1910
- display: flex;
1911
- flex-direction: column;
1912
- gap: 8px;
1913
- padding: 16px;
1914
- background: rgba(255, 255, 255, 0.95);
1915
- border: 1px solid rgba(0, 0, 0, 0.08);
1916
- border-radius: 12px;
1917
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1918
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1919
- }
1920
-
1921
- .backend-info-item:hover {
1922
- background: rgba(255, 255, 255, 1);
1923
- border-color: rgba(59, 130, 246, 0.3);
1924
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
1925
- }
1926
-
1927
- .info-label {
1928
- font-size: 0.75rem;
1929
- font-weight: 700;
1930
- color: #64748b;
1931
- text-transform: uppercase;
1932
- letter-spacing: 0.08em;
1933
- }
1934
-
1935
- .info-value {
1936
- font-size: 1.125rem;
1937
- font-weight: 700;
1938
- color: #0f172a;
1939
- font-family: 'Manrope', sans-serif;
1940
- }
1941
-
1942
- .fear-greed-card {
1943
- position: relative;
1944
- overflow: hidden;
1945
- }
1946
-
1947
- .fear-greed-card::before {
1948
- content: '';
1949
- position: absolute;
1950
- top: 0;
1951
- left: 0;
1952
- right: 0;
1953
- height: 3px;
1954
- background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
1955
- opacity: 0.6;
1956
- }
1957
-
1958
- .fear-greed-value {
1959
- font-size: 2.5rem !important;
1960
- font-weight: 800 !important;
1961
- line-height: 1;
1962
- }
1963
-
1964
- .fear-greed-classification {
1965
- font-size: 0.875rem;
1966
- font-weight: 600;
1967
- margin-top: 8px;
1968
- text-transform: uppercase;
1969
- letter-spacing: 0.05em;
1970
- }
1971
-
1972
- .fear-greed-gauge {
1973
- margin-top: 16px;
1974
- }
1975
-
1976
- .gauge-bar {
1977
- background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
1978
- height: 8px;
1979
- border-radius: 4px;
1980
- position: relative;
1981
- overflow: visible;
1982
- }
1983
-
1984
- .gauge-indicator {
1985
- position: absolute;
1986
- top: 50%;
1987
- transform: translate(-50%, -50%);
1988
- width: 16px;
1989
- height: 16px;
1990
- border: 2px solid #fff;
1991
- border-radius: 50%;
1992
- box-shadow: 0 0 8px currentColor;
1993
- transition: left 0.3s ease;
1994
- }
1995
-
1996
- .gauge-labels {
1997
- display: flex;
1998
- justify-content: space-between;
1999
- margin-top: 8px;
2000
- font-size: 0.75rem;
2001
- color: var(--text-muted);
2002
- }
2003
-
2004
  .toggle {
2005
  position: relative;
2006
  width: 44px;
@@ -2040,665 +541,11 @@ button.ghost.active::before {
2040
  100% { background: transparent; }
2041
  }
2042
 
2043
- .table-container {
2044
- overflow-x: auto;
2045
- border-radius: 16px;
2046
- background: rgba(255, 255, 255, 0.02);
2047
- border: 1px solid var(--glass-border);
2048
- }
2049
-
2050
- .chip {
2051
- display: inline-flex;
2052
- align-items: center;
2053
- padding: 6px 12px;
2054
- border-radius: 999px;
2055
- background: rgba(255, 255, 255, 0.08);
2056
- font-size: 0.8125rem;
2057
- font-weight: 500;
2058
- font-family: 'Inter', sans-serif;
2059
- color: var(--text-primary);
2060
- }
2061
-
2062
- /* Modern Sentiment UI - Professional Design */
2063
- .sentiment-modern {
2064
- display: flex;
2065
- flex-direction: column;
2066
- gap: 1.75rem;
2067
- }
2068
-
2069
- .sentiment-header {
2070
- display: flex;
2071
- justify-content: space-between;
2072
- align-items: center;
2073
- margin-bottom: 0.75rem;
2074
- padding-bottom: 1rem;
2075
- border-bottom: 2px solid rgba(255, 255, 255, 0.1);
2076
- }
2077
-
2078
- .sentiment-header h4 {
2079
- margin: 0;
2080
- font-size: 1.25rem;
2081
- font-weight: 700;
2082
- font-family: 'Manrope', 'DM Sans', sans-serif;
2083
- letter-spacing: -0.02em;
2084
- background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
2085
- -webkit-background-clip: text;
2086
- -webkit-text-fill-color: transparent;
2087
- background-clip: text;
2088
- }
2089
-
2090
- .sentiment-badge {
2091
- display: inline-flex;
2092
- align-items: center;
2093
- gap: 0.5rem;
2094
- padding: 6px 14px;
2095
- border-radius: 999px;
2096
- background: linear-gradient(135deg, rgba(143, 136, 255, 0.25), rgba(22, 217, 250, 0.25));
2097
- border: 1px solid rgba(143, 136, 255, 0.4);
2098
- font-size: 0.75rem;
2099
- font-weight: 700;
2100
- text-transform: uppercase;
2101
- letter-spacing: 0.1em;
2102
- color: var(--primary-light);
2103
- box-shadow: 0 4px 12px rgba(143, 136, 255, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
2104
- font-family: 'Manrope', sans-serif;
2105
- }
2106
-
2107
- .sentiment-cards {
2108
- display: flex;
2109
- flex-direction: column;
2110
- gap: 1.25rem;
2111
- }
2112
-
2113
- .sentiment-item {
2114
- display: flex;
2115
- flex-direction: column;
2116
- gap: 0.75rem;
2117
- padding: 1.25rem;
2118
- background: rgba(255, 255, 255, 0.04);
2119
- border-radius: 16px;
2120
- border: 1px solid rgba(255, 255, 255, 0.08);
2121
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2122
- position: relative;
2123
- overflow: hidden;
2124
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05);
2125
- }
2126
-
2127
- .sentiment-item::before {
2128
- content: '';
2129
- position: absolute;
2130
- top: 0;
2131
- left: 0;
2132
- width: 4px;
2133
- height: 100%;
2134
- background: currentColor;
2135
- opacity: 0.6;
2136
- transform: scaleY(0);
2137
- transform-origin: bottom;
2138
- transition: transform 0.3s ease;
2139
- }
2140
-
2141
- .sentiment-item:hover {
2142
- background: rgba(255, 255, 255, 0.08);
2143
- border-color: rgba(255, 255, 255, 0.15);
2144
- transform: translateX(6px) translateY(-2px);
2145
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2146
- }
2147
-
2148
- .sentiment-item:hover::before {
2149
- transform: scaleY(1);
2150
- }
2151
-
2152
- .sentiment-item-header {
2153
- display: flex;
2154
- align-items: center;
2155
- gap: 0.75rem;
2156
- }
2157
-
2158
- .sentiment-icon {
2159
- display: flex;
2160
- align-items: center;
2161
- justify-content: center;
2162
- width: 40px;
2163
- height: 40px;
2164
- border-radius: 12px;
2165
- flex-shrink: 0;
2166
- transition: all 0.3s ease;
2167
- }
2168
-
2169
- .sentiment-item:hover .sentiment-icon {
2170
- transform: scale(1.15) rotate(5deg);
2171
- }
2172
-
2173
- .sentiment-item.bullish {
2174
- color: #22c55e;
2175
- }
2176
-
2177
- .sentiment-item.bullish .sentiment-icon {
2178
- background: linear-gradient(135deg, rgba(34, 197, 94, 0.25), rgba(16, 185, 129, 0.2));
2179
- color: #22c55e;
2180
- border: 1px solid rgba(34, 197, 94, 0.3);
2181
- box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2182
- }
2183
-
2184
- .sentiment-item.neutral {
2185
- color: #38bdf8;
2186
- }
2187
-
2188
- .sentiment-item.neutral .sentiment-icon {
2189
- background: linear-gradient(135deg, rgba(56, 189, 248, 0.25), rgba(14, 165, 233, 0.2));
2190
- color: #38bdf8;
2191
- border: 1px solid rgba(56, 189, 248, 0.3);
2192
- box-shadow: 0 4px 12px rgba(56, 189, 248, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2193
- }
2194
-
2195
- .sentiment-item.bearish {
2196
- color: #ef4444;
2197
- }
2198
-
2199
- .sentiment-item.bearish .sentiment-icon {
2200
- background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.2));
2201
- color: #ef4444;
2202
- border: 1px solid rgba(239, 68, 68, 0.3);
2203
- box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
2204
- }
2205
-
2206
- .sentiment-label {
2207
- flex: 1;
2208
- font-size: 1rem;
2209
- font-weight: 600;
2210
- font-family: 'Manrope', 'DM Sans', sans-serif;
2211
- color: var(--text-primary);
2212
- letter-spacing: -0.01em;
2213
- }
2214
-
2215
- .sentiment-percent {
2216
- font-size: 1.125rem;
2217
- font-weight: 800;
2218
- font-family: 'Manrope', 'DM Sans', sans-serif;
2219
- color: var(--text-primary);
2220
- letter-spacing: -0.02em;
2221
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
2222
- }
2223
-
2224
- .sentiment-progress {
2225
- width: 100%;
2226
- height: 10px;
2227
- background: rgba(0, 0, 0, 0.3);
2228
- border-radius: 999px;
2229
- overflow: hidden;
2230
- position: relative;
2231
- border: 1px solid rgba(255, 255, 255, 0.05);
2232
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
2233
- }
2234
-
2235
- .sentiment-progress-bar {
2236
- height: 100%;
2237
- border-radius: 999px;
2238
- transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
2239
- box-shadow: 0 0 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2);
2240
- position: relative;
2241
- overflow: hidden;
2242
- }
2243
-
2244
- .sentiment-progress-bar::after {
2245
- content: '';
2246
- position: absolute;
2247
- top: 0;
2248
- left: 0;
2249
- right: 0;
2250
- bottom: 0;
2251
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
2252
- animation: shimmer 2s infinite;
2253
- }
2254
-
2255
- @keyframes shimmer {
2256
- 0% { transform: translateX(-100%); }
2257
- 100% { transform: translateX(100%); }
2258
- }
2259
-
2260
- .sentiment-summary {
2261
- display: flex;
2262
- gap: 2rem;
2263
- padding: 1.25rem;
2264
- background: rgba(255, 255, 255, 0.03);
2265
- border-radius: 14px;
2266
- border: 1px solid rgba(255, 255, 255, 0.08);
2267
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
2268
- }
2269
-
2270
- .sentiment-summary-item {
2271
- display: flex;
2272
- flex-direction: column;
2273
- gap: 0.5rem;
2274
- flex: 1;
2275
- }
2276
-
2277
- .summary-label {
2278
- font-size: 0.75rem;
2279
- text-transform: uppercase;
2280
- letter-spacing: 0.1em;
2281
- color: var(--text-muted);
2282
- font-weight: 600;
2283
- font-family: 'Manrope', sans-serif;
2284
- }
2285
-
2286
- .summary-value {
2287
- font-size: 1.5rem;
2288
- font-weight: 800;
2289
- font-family: 'Manrope', 'DM Sans', sans-serif;
2290
- letter-spacing: -0.02em;
2291
- text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
2292
- }
2293
-
2294
- .summary-value.bullish {
2295
- color: #22c55e;
2296
- text-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
2297
- }
2298
-
2299
- .summary-value.neutral {
2300
- color: #38bdf8;
2301
- text-shadow: 0 0 20px rgba(56, 189, 248, 0.4);
2302
- }
2303
-
2304
- .summary-value.bearish {
2305
- color: #ef4444;
2306
- text-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
2307
- }
2308
-
2309
  @keyframes fadeIn {
2310
  from { opacity: 0; transform: translateY(8px); }
2311
  to { opacity: 1; transform: translateY(0); }
2312
  }
2313
 
2314
- /* Chart Lab Styles */
2315
- .chart-controls {
2316
- display: flex;
2317
- flex-direction: column;
2318
- gap: 1.5rem;
2319
- padding: 1.5rem;
2320
- background: var(--glass-bg);
2321
- border: 1px solid var(--glass-border);
2322
- border-radius: 20px;
2323
- backdrop-filter: blur(20px);
2324
- }
2325
-
2326
- .chart-label {
2327
- display: block;
2328
- font-size: 0.8125rem;
2329
- font-weight: 600;
2330
- text-transform: uppercase;
2331
- letter-spacing: 0.05em;
2332
- color: var(--text-muted);
2333
- margin-bottom: 0.5rem;
2334
- font-family: 'Manrope', sans-serif;
2335
- }
2336
-
2337
- .chart-symbol-selector {
2338
- flex: 1;
2339
- }
2340
-
2341
- .combobox-wrapper {
2342
- position: relative;
2343
- }
2344
-
2345
- .combobox-input {
2346
- width: 100%;
2347
- padding: 12px 16px;
2348
- background: rgba(255, 255, 255, 0.05);
2349
- border: 1px solid var(--glass-border);
2350
- border-radius: 12px;
2351
- color: var(--text-primary);
2352
- font-family: 'Manrope', 'DM Sans', sans-serif;
2353
- font-size: 0.9375rem;
2354
- transition: all 0.2s ease;
2355
- }
2356
-
2357
- .combobox-input:focus {
2358
- outline: none;
2359
- border-color: var(--primary);
2360
- background: rgba(255, 255, 255, 0.08);
2361
- box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
2362
- }
2363
-
2364
- .combobox-dropdown {
2365
- position: absolute;
2366
- top: 100%;
2367
- left: 0;
2368
- right: 0;
2369
- margin-top: 0.5rem;
2370
- background: var(--glass-bg);
2371
- border: 1px solid var(--glass-border);
2372
- border-radius: 12px;
2373
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
2374
- backdrop-filter: blur(20px);
2375
- z-index: 100;
2376
- max-height: 300px;
2377
- overflow-y: auto;
2378
- }
2379
-
2380
- .combobox-options {
2381
- padding: 0.5rem;
2382
- }
2383
-
2384
- .combobox-option {
2385
- display: flex;
2386
- justify-content: space-between;
2387
- align-items: center;
2388
- padding: 10px 14px;
2389
- border-radius: 8px;
2390
- cursor: pointer;
2391
- transition: all 0.2s ease;
2392
- font-family: 'Manrope', sans-serif;
2393
- }
2394
-
2395
- .combobox-option:hover {
2396
- background: rgba(255, 255, 255, 0.1);
2397
- transform: translateX(4px);
2398
- }
2399
-
2400
- .combobox-option.disabled {
2401
- opacity: 0.5;
2402
- cursor: not-allowed;
2403
- }
2404
-
2405
- .combobox-option strong {
2406
- font-weight: 700;
2407
- color: var(--text-primary);
2408
- font-size: 0.9375rem;
2409
- }
2410
-
2411
- .combobox-option span {
2412
- color: var(--text-muted);
2413
- font-size: 0.875rem;
2414
- }
2415
-
2416
- .chart-timeframe-selector {
2417
- display: flex;
2418
- flex-direction: column;
2419
- gap: 0.5rem;
2420
- }
2421
-
2422
- .chart-actions {
2423
- display: flex;
2424
- align-items: flex-end;
2425
- }
2426
-
2427
- .chart-container {
2428
- padding: 1.5rem;
2429
- background: rgba(0, 0, 0, 0.15);
2430
- border-radius: 16px;
2431
- border: 1px solid rgba(255, 255, 255, 0.05);
2432
- }
2433
-
2434
- .chart-header {
2435
- display: flex;
2436
- justify-content: space-between;
2437
- align-items: center;
2438
- margin-bottom: 1.5rem;
2439
- padding-bottom: 1rem;
2440
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2441
- }
2442
-
2443
- .chart-header h4 {
2444
- margin: 0;
2445
- }
2446
-
2447
- .chart-legend {
2448
- display: flex;
2449
- gap: 2rem;
2450
- flex-wrap: wrap;
2451
- }
2452
-
2453
- .legend-item {
2454
- display: flex;
2455
- flex-direction: column;
2456
- gap: 0.25rem;
2457
- }
2458
-
2459
- .legend-label {
2460
- font-size: 0.7rem;
2461
- text-transform: uppercase;
2462
- letter-spacing: 0.08em;
2463
- color: rgba(226, 232, 240, 0.5);
2464
- font-weight: 600;
2465
- font-family: 'Manrope', sans-serif;
2466
- }
2467
-
2468
- .legend-value {
2469
- font-size: 1rem;
2470
- font-weight: 600;
2471
- font-family: 'Manrope', 'DM Sans', sans-serif;
2472
- color: var(--text-primary);
2473
- display: flex;
2474
- align-items: center;
2475
- gap: 0.25rem;
2476
- }
2477
-
2478
- .legend-arrow {
2479
- font-size: 0.875rem;
2480
- opacity: 0.8;
2481
- }
2482
-
2483
- .legend-value.positive {
2484
- color: #26a69a;
2485
- }
2486
-
2487
- .legend-value.negative {
2488
- color: #ef5350;
2489
- }
2490
-
2491
- .chart-wrapper {
2492
- position: relative;
2493
- height: 450px;
2494
- padding: 0;
2495
- background: rgba(0, 0, 0, 0.2);
2496
- border-radius: 12px;
2497
- overflow: hidden;
2498
- }
2499
-
2500
- .chart-wrapper canvas {
2501
- padding: 12px;
2502
- }
2503
-
2504
- .chart-loading {
2505
- position: absolute;
2506
- inset: 0;
2507
- display: flex;
2508
- flex-direction: column;
2509
- align-items: center;
2510
- justify-content: center;
2511
- gap: 1rem;
2512
- background: rgba(0, 0, 0, 0.3);
2513
- border-radius: 12px;
2514
- z-index: 10;
2515
- }
2516
-
2517
- .loading-spinner {
2518
- width: 40px;
2519
- height: 40px;
2520
- border: 3px solid rgba(255, 255, 255, 0.1);
2521
- border-top-color: var(--primary);
2522
- border-radius: 50%;
2523
- animation: spin 1s linear infinite;
2524
- }
2525
-
2526
- @keyframes spin {
2527
- to { transform: rotate(360deg); }
2528
- }
2529
-
2530
- .indicator-selector {
2531
- margin-top: 1rem;
2532
- }
2533
-
2534
- .indicator-selector .button-group {
2535
- display: grid;
2536
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
2537
- gap: 0.75rem;
2538
- }
2539
-
2540
- .indicator-selector button {
2541
- display: flex;
2542
- flex-direction: column;
2543
- align-items: center;
2544
- gap: 0.25rem;
2545
- padding: 12px 16px;
2546
- }
2547
-
2548
- .indicator-selector button span {
2549
- font-weight: 600;
2550
- font-size: 0.9375rem;
2551
- }
2552
-
2553
- .indicator-selector button small {
2554
- font-size: 0.75rem;
2555
- opacity: 0.7;
2556
- font-weight: 400;
2557
- }
2558
-
2559
- .analysis-output {
2560
- margin-top: 1.5rem;
2561
- padding-top: 1.5rem;
2562
- border-top: 1px solid rgba(255, 255, 255, 0.1);
2563
- }
2564
-
2565
- .analysis-loading {
2566
- display: flex;
2567
- flex-direction: column;
2568
- align-items: center;
2569
- justify-content: center;
2570
- gap: 1rem;
2571
- padding: 2rem;
2572
- }
2573
-
2574
- .analysis-results {
2575
- display: flex;
2576
- flex-direction: column;
2577
- gap: 1.5rem;
2578
- }
2579
-
2580
- .analysis-header {
2581
- display: flex;
2582
- justify-content: space-between;
2583
- align-items: center;
2584
- padding-bottom: 1rem;
2585
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
2586
- }
2587
-
2588
- .analysis-header h5 {
2589
- margin: 0;
2590
- font-size: 1.125rem;
2591
- font-weight: 700;
2592
- font-family: 'Manrope', 'DM Sans', sans-serif;
2593
- }
2594
-
2595
- .analysis-badge {
2596
- padding: 4px 12px;
2597
- border-radius: 999px;
2598
- font-size: 0.75rem;
2599
- font-weight: 700;
2600
- text-transform: uppercase;
2601
- letter-spacing: 0.05em;
2602
- }
2603
-
2604
- .analysis-badge.bullish {
2605
- background: rgba(34, 197, 94, 0.2);
2606
- color: var(--success);
2607
- border: 1px solid rgba(34, 197, 94, 0.3);
2608
- }
2609
-
2610
- .analysis-badge.bearish {
2611
- background: rgba(239, 68, 68, 0.2);
2612
- color: var(--danger);
2613
- border: 1px solid rgba(239, 68, 68, 0.3);
2614
- }
2615
-
2616
- .analysis-badge.neutral {
2617
- background: rgba(56, 189, 248, 0.2);
2618
- color: var(--info);
2619
- border: 1px solid rgba(56, 189, 248, 0.3);
2620
- }
2621
-
2622
- .analysis-metrics {
2623
- display: grid;
2624
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
2625
- gap: 1rem;
2626
- }
2627
-
2628
- .metric-item {
2629
- display: flex;
2630
- flex-direction: column;
2631
- gap: 0.5rem;
2632
- padding: 1rem;
2633
- background: rgba(255, 255, 255, 0.03);
2634
- border-radius: 12px;
2635
- border: 1px solid rgba(255, 255, 255, 0.05);
2636
- }
2637
-
2638
- .metric-label {
2639
- font-size: 0.75rem;
2640
- text-transform: uppercase;
2641
- letter-spacing: 0.05em;
2642
- color: var(--text-muted);
2643
- font-weight: 600;
2644
- }
2645
-
2646
- .metric-value {
2647
- font-size: 1.25rem;
2648
- font-weight: 700;
2649
- font-family: 'Manrope', 'DM Sans', sans-serif;
2650
- color: var(--text-primary);
2651
- }
2652
-
2653
- .metric-value.positive {
2654
- color: var(--success);
2655
- }
2656
-
2657
- .metric-value.negative {
2658
- color: var(--danger);
2659
- }
2660
-
2661
- .analysis-summary,
2662
- .analysis-signals {
2663
- padding: 1rem;
2664
- background: rgba(255, 255, 255, 0.03);
2665
- border-radius: 12px;
2666
- border: 1px solid rgba(255, 255, 255, 0.05);
2667
- }
2668
-
2669
- .analysis-summary h6,
2670
- .analysis-signals h6 {
2671
- margin: 0 0 0.75rem 0;
2672
- font-size: 0.9375rem;
2673
- font-weight: 600;
2674
- text-transform: uppercase;
2675
- letter-spacing: 0.05em;
2676
- color: var(--text-muted);
2677
- }
2678
-
2679
- .analysis-summary p {
2680
- margin: 0;
2681
- line-height: 1.6;
2682
- color: var(--text-secondary);
2683
- }
2684
-
2685
- .analysis-signals ul {
2686
- margin: 0;
2687
- padding-left: 1.5rem;
2688
- list-style: disc;
2689
- }
2690
-
2691
- .analysis-signals li {
2692
- margin-bottom: 0.5rem;
2693
- color: var(--text-secondary);
2694
- line-height: 1.6;
2695
- }
2696
-
2697
- .analysis-signals li strong {
2698
- color: var(--text-primary);
2699
- font-weight: 600;
2700
- }
2701
-
2702
  @keyframes shimmer {
2703
  100% { transform: translateX(100%); }
2704
  }
 
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'] {
 
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 {
 
92
 
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 {
 
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 {
 
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 {
 
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 {
 
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 {
 
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 {
 
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 {
 
502
  font-size: 0.75rem;
503
  }
504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  .toggle {
506
  position: relative;
507
  width: 44px;
 
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 {
550
  100% { transform: translateX(100%); }
551
  }
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
- // ==== آدرس صحیح بک‌اند شما (Really-amin) ====
6
- this.baseURL = 'https://really-amin-datasourceforcryptocurrency.hf.space';
7
-
8
- // اگر بعداً بخوای دستی عوض کنی، از HTML استفاده کن
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,110 +153,41 @@ class ApiClient {
203
  }
204
 
205
  getPriceChart(symbol, timeframe = '7d') {
206
- // Backend uses /api/market/history
207
- const cleanSymbol = encodeURIComponent(String(symbol || 'BTC').trim().toUpperCase());
208
- // Map timeframe to limit (rough approximation)
209
- const limitMap = { '1d': 24, '7d': 168, '30d': 720, '90d': 2160, '365d': 8760 };
210
- const limit = limitMap[timeframe] || 168;
211
- return this.get(`/api/market/history?symbol=${cleanSymbol}&limit=${limit}`);
212
  }
213
 
214
  analyzeChart(symbol, timeframe = '7d', indicators = []) {
215
- // Not implemented in backend yet
216
- return Promise.resolve({
217
- ok: false,
218
- error: 'Chart analysis not yet implemented in backend'
219
- });
220
  }
221
 
222
  runQuery(payload) {
223
- // Not implemented in backend yet
224
- return Promise.resolve({
225
- ok: false,
226
- error: 'Query endpoint not yet implemented in backend'
227
- });
228
  }
229
 
230
  analyzeSentiment(payload) {
231
- // Backend has /api/sentiment but it returns market sentiment, not text analysis
232
- // For now, return the market sentiment
233
- return this.get('/api/sentiment');
234
  }
235
 
236
  summarizeNews(item) {
237
- // Not implemented in backend yet
238
- return Promise.resolve({
239
- ok: false,
240
- error: 'News summarization not yet implemented in backend'
241
- });
242
  }
243
 
244
  getDatasetsList() {
245
- // Not implemented in backend yet
246
- return Promise.resolve({
247
- ok: true,
248
- data: {
249
- datasets: [],
250
- message: 'Datasets endpoint not yet implemented in backend'
251
- }
252
- });
253
  }
254
 
255
  getDatasetSample(name) {
256
- // Not implemented in backend yet
257
- return Promise.resolve({
258
- ok: false,
259
- error: 'Dataset sample not yet implemented in backend'
260
- });
261
  }
262
 
263
  getModelsList() {
264
- // Backend has /api/hf/models
265
- return this.get('/api/hf/models');
266
  }
267
 
268
  testModel(payload) {
269
- // Not implemented in backend yet
270
- return Promise.resolve({
271
- ok: false,
272
- error: 'Model testing not yet implemented in backend'
273
- });
274
- }
275
-
276
- // ===== Additional methods for backend compatibility =====
277
-
278
- getTrending() {
279
- return this.get('/api/trending');
280
- }
281
-
282
- getStats() {
283
- return this.get('/api/stats');
284
- }
285
-
286
- getHFHealth() {
287
- return this.get('/api/hf/health');
288
- }
289
-
290
- runDiagnostics(autoFix = false) {
291
- return this.post('/api/diagnostics/run', { auto_fix: autoFix });
292
- }
293
-
294
- getLastDiagnostics() {
295
- return this.get('/api/diagnostics/last');
296
- }
297
-
298
- runAPLScan() {
299
- return this.post('/api/apl/run');
300
- }
301
-
302
- getAPLReport() {
303
- return this.get('/api/apl/report');
304
- }
305
-
306
- getAPLSummary() {
307
- return this.get('/api/apl/summary');
308
  }
309
  }
310
 
311
  const apiClient = new ApiClient();
312
- 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,955 +1,98 @@
1
- /**
2
- * ═══════════════════════════════════════════════════════════════════
3
- * HTS CRYPTO DASHBOARD - UNIFIED APPLICATION
4
- * Complete JavaScript Logic with WebSocket & API Integration
5
- * ═══════════════════════════════════════════════════════════════════
6
- */
7
-
8
- // ═══════════════════════════════════════════════════════════════════
9
- // CONFIGURATION
10
- // ═══════════════════════════════════════════════════════════════════
11
-
12
- const CONFIG = {
13
- BACKEND_URL: 'https://really-amin-datasourceforcryptocurrency.hf.space',
14
- WS_URL: 'wss://really-amin-datasourceforcryptocurrency.hf.space/ws',
15
- UPDATE_INTERVAL: 30000, // 30 seconds
16
- CACHE_TTL: 60000, // 1 minute
17
- };
18
-
19
- // ═══════════════════════════════════════════════════════════════════
20
- // WEBSOCKET CLIENT
21
- // ═══════════════════════════════════════════════════════════════════
22
-
23
- class WebSocketClient {
24
- constructor(url) {
25
- this.url = url;
26
- this.socket = null;
27
- this.status = 'disconnected';
28
- this.reconnectAttempts = 0;
29
- this.maxReconnectAttempts = 5;
30
- this.reconnectDelay = 3000;
31
- this.listeners = new Map();
32
- this.heartbeatInterval = null;
33
- }
34
-
35
- connect() {
36
- if (this.socket && this.socket.readyState === WebSocket.OPEN) {
37
- console.log('[WS] Already connected');
38
- return;
39
- }
40
-
41
- try {
42
- console.log('[WS] Connecting to:', this.url);
43
- this.socket = new WebSocket(this.url);
44
-
45
- this.socket.onopen = this.handleOpen.bind(this);
46
- this.socket.onmessage = this.handleMessage.bind(this);
47
- this.socket.onerror = this.handleError.bind(this);
48
- this.socket.onclose = this.handleClose.bind(this);
49
-
50
- this.updateStatus('connecting');
51
- } catch (error) {
52
- console.error('[WS] Connection error:', error);
53
- this.scheduleReconnect();
54
- }
55
- }
56
-
57
- handleOpen() {
58
- console.log('[WS] Connected successfully');
59
- this.status = 'connected';
60
- this.reconnectAttempts = 0;
61
- this.updateStatus('connected');
62
- this.startHeartbeat();
63
- this.emit('connected', true);
64
- }
65
-
66
- handleMessage(event) {
67
- try {
68
- const data = JSON.parse(event.data);
69
- console.log('[WS] Message received:', data.type);
70
-
71
- if (data.type === 'heartbeat') {
72
- this.send({ type: 'pong' });
73
- return;
74
- }
75
-
76
- this.emit(data.type, data);
77
- this.emit('message', data);
78
- } catch (error) {
79
- console.error('[WS] Message parse error:', error);
80
- }
81
- }
82
-
83
- handleError(error) {
84
- console.error('[WS] Error:', error);
85
- this.updateStatus('error');
86
- }
87
-
88
- handleClose() {
89
- console.log('[WS] Connection closed');
90
- this.status = 'disconnected';
91
- this.updateStatus('disconnected');
92
- this.stopHeartbeat();
93
- this.emit('connected', false);
94
- this.scheduleReconnect();
95
- }
96
-
97
- scheduleReconnect() {
98
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
99
- console.error('[WS] Max reconnection attempts reached');
100
- return;
101
- }
102
-
103
- this.reconnectAttempts++;
104
- console.log(`[WS] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
105
-
106
- setTimeout(() => this.connect(), this.reconnectDelay);
107
- }
108
-
109
- startHeartbeat() {
110
- this.heartbeatInterval = setInterval(() => {
111
- if (this.isConnected()) {
112
- this.send({ type: 'ping' });
113
- }
114
- }, 30000);
115
- }
116
-
117
- stopHeartbeat() {
118
- if (this.heartbeatInterval) {
119
- clearInterval(this.heartbeatInterval);
120
- this.heartbeatInterval = null;
121
- }
122
- }
123
-
124
- send(data) {
125
- if (this.isConnected()) {
126
- this.socket.send(JSON.stringify(data));
127
- return true;
128
- }
129
- console.warn('[WS] Cannot send - not connected');
130
- return false;
131
- }
132
-
133
- on(event, callback) {
134
- if (!this.listeners.has(event)) {
135
- this.listeners.set(event, []);
136
- }
137
- this.listeners.get(event).push(callback);
138
- }
139
-
140
- emit(event, data) {
141
- if (this.listeners.has(event)) {
142
- this.listeners.get(event).forEach(callback => callback(data));
143
- }
144
- }
145
-
146
- updateStatus(status) {
147
- this.status = status;
148
-
149
- const statusBar = document.getElementById('connection-status-bar');
150
- const statusDot = document.getElementById('ws-status-dot');
151
- const statusText = document.getElementById('ws-status-text');
152
-
153
- if (statusBar && statusDot && statusText) {
154
- if (status === 'connected') {
155
- statusBar.classList.remove('disconnected');
156
- statusText.textContent = 'متصل';
157
- } else if (status === 'disconnected' || status === 'error') {
158
- statusBar.classList.add('disconnected');
159
- statusText.textContent = status === 'error' ? 'خطا در اتصال' : 'قطع شده';
160
- } else {
161
- statusText.textContent = 'در حال اتصال...';
162
- }
163
- }
164
- }
165
-
166
- isConnected() {
167
- return this.socket && this.socket.readyState === WebSocket.OPEN;
168
- }
169
-
170
- disconnect() {
171
- if (this.socket) {
172
- this.socket.close();
173
- }
174
- this.stopHeartbeat();
175
- }
176
- }
177
-
178
- // ═══════════════════════════════════════════════════════════════════
179
- // API CLIENT
180
- // ═══════════════════════════════════════════════════════════════════
181
-
182
- class APIClient {
183
- constructor(baseURL) {
184
- this.baseURL = baseURL;
185
- this.cache = new Map();
186
- }
187
-
188
- async request(endpoint, options = {}) {
189
- const url = `${this.baseURL}${endpoint}`;
190
- const cacheKey = `${options.method || 'GET'}:${url}`;
191
 
192
- // Check cache
193
- if (options.cache && this.cache.has(cacheKey)) {
194
- const cached = this.cache.get(cacheKey);
195
- if (Date.now() - cached.timestamp < CONFIG.CACHE_TTL) {
196
- console.log('[API] Cache hit:', endpoint);
197
- return cached.data;
198
- }
199
- }
200
 
201
- try {
202
- console.log('[API] Request:', endpoint);
203
- const response = await fetch(url, {
204
- method: options.method || 'GET',
205
- headers: {
206
- 'Content-Type': 'application/json',
207
- ...options.headers,
208
- },
209
- body: options.body ? JSON.stringify(options.body) : undefined,
210
  });
 
 
211
 
212
- if (!response.ok) {
213
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
214
- }
215
-
216
- const data = await response.json();
217
-
218
- // Cache successful GET requests
219
- if (!options.method || options.method === 'GET') {
220
- this.cache.set(cacheKey, {
221
- data,
222
- timestamp: Date.now(),
223
- });
224
- }
225
-
226
- return data;
227
- } catch (error) {
228
- console.error('[API] Error:', endpoint, error);
229
- throw error;
230
- }
231
- }
232
 
233
- // Market Data
234
- async getMarket() {
235
- return this.request('/api/market', { cache: true });
236
- }
237
 
238
- async getTrending() {
239
- return this.request('/api/trending', { cache: true });
240
- }
241
 
242
- async getSentiment() {
243
- return this.request('/api/sentiment', { cache: true });
244
- }
245
 
246
- async getStats() {
247
- return this.request('/api/stats', { cache: true });
248
- }
249
 
250
- // News
251
- async getNews(limit = 20) {
252
- return this.request(`/api/news?limit=${limit}`, { cache: true });
253
- }
254
 
255
- // Providers
256
- async getProviders() {
257
- return this.request('/api/providers', { cache: true });
258
- }
259
 
260
- // Chart Data
261
- async getChartData(symbol, interval = '1h', limit = 100) {
262
- return this.request(`/api/market/history?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true });
263
- }
264
- }
265
 
266
- // ═══════════════════════════════════════════════════════════════════
267
- // UTILITY FUNCTIONS
268
- // ═══════════════════════════════════════════════════════════════════
269
 
270
- const Utils = {
271
- formatCurrency(value) {
272
- if (value === null || value === undefined || isNaN(value)) {
273
- return '—';
274
- }
275
- const num = Number(value);
276
- if (Math.abs(num) >= 1e12) {
277
- return `$${(num / 1e12).toFixed(2)}T`;
278
- }
279
- if (Math.abs(num) >= 1e9) {
280
- return `$${(num / 1e9).toFixed(2)}B`;
281
- }
282
- if (Math.abs(num) >= 1e6) {
283
- return `$${(num / 1e6).toFixed(2)}M`;
284
- }
285
- if (Math.abs(num) >= 1e3) {
286
- return `$${(num / 1e3).toFixed(2)}K`;
287
- }
288
- return `$${num.toLocaleString(undefined, {
289
- minimumFractionDigits: 2,
290
- maximumFractionDigits: 2
291
- })}`;
292
  },
293
 
294
- formatPercent(value) {
295
- if (value === null || value === undefined || isNaN(value)) {
296
- return '—';
297
- }
298
- const num = Number(value);
299
- const sign = num >= 0 ? '+' : '';
300
- return `${sign}${num.toFixed(2)}%`;
301
- },
302
-
303
- formatNumber(value) {
304
- if (value === null || value === undefined || isNaN(value)) {
305
- return '—';
306
- }
307
- return Number(value).toLocaleString();
308
- },
309
-
310
- formatDate(timestamp) {
311
- const date = new Date(timestamp);
312
- return date.toLocaleDateString('fa-IR', {
313
- year: 'numeric',
314
- month: 'long',
315
- day: 'numeric',
316
- hour: '2-digit',
317
- minute: '2-digit',
318
  });
319
  },
320
 
321
- getChangeClass(value) {
322
- if (value > 0) return 'positive';
323
- if (value < 0) return 'negative';
324
- return 'neutral';
325
- },
326
-
327
- showLoader(element) {
328
- if (element) {
329
- element.innerHTML = `
330
- <div class="loading-cell">
331
- <div class="loader"></div>
332
- در حال بارگذاری...
333
- </div>
334
- `;
335
- }
336
- },
337
-
338
- showError(element, message) {
339
- if (element) {
340
- element.innerHTML = `
341
- <div class="error-message">
342
- <i class="fas fa-exclamation-circle"></i>
343
- ${message}
344
- </div>
345
- `;
346
  }
347
  },
348
-
349
- debounce(func, wait) {
350
- let timeout;
351
- return function executedFunction(...args) {
352
- const later = () => {
353
- clearTimeout(timeout);
354
- func(...args);
355
- };
356
- clearTimeout(timeout);
357
- timeout = setTimeout(later, wait);
358
- };
359
- },
360
  };
361
 
362
- // ═══════════════════════════════════════════════════════════════════
363
- // VIEW MANAGER
364
- // ═══════════════════════════════════════════════════════════════════
365
-
366
- class ViewManager {
367
- constructor() {
368
- this.currentView = 'overview';
369
- this.views = new Map();
370
- this.init();
371
- }
372
-
373
- init() {
374
- // Desktop navigation
375
- document.querySelectorAll('.nav-tab-btn').forEach(btn => {
376
- btn.addEventListener('click', (e) => {
377
- const view = btn.dataset.view;
378
- this.switchView(view);
379
- });
380
- });
381
-
382
- // Mobile navigation
383
- document.querySelectorAll('.mobile-nav-tab-btn').forEach(btn => {
384
- btn.addEventListener('click', (e) => {
385
- const view = btn.dataset.view;
386
- this.switchView(view);
387
- });
388
- });
389
- }
390
-
391
- switchView(viewName) {
392
- if (this.currentView === viewName) return;
393
-
394
- // Hide all views
395
- document.querySelectorAll('.view-section').forEach(section => {
396
- section.classList.remove('active');
397
- });
398
-
399
- // Show selected view
400
- const viewSection = document.getElementById(`view-${viewName}`);
401
- if (viewSection) {
402
- viewSection.classList.add('active');
403
- }
404
-
405
- // Update navigation buttons
406
- document.querySelectorAll('.nav-tab-btn, .mobile-nav-tab-btn').forEach(btn => {
407
- btn.classList.remove('active');
408
- if (btn.dataset.view === viewName) {
409
- btn.classList.add('active');
410
- }
411
- });
412
-
413
- this.currentView = viewName;
414
- console.log('[View] Switched to:', viewName);
415
-
416
- // Trigger view-specific updates
417
- this.triggerViewUpdate(viewName);
418
- }
419
-
420
- triggerViewUpdate(viewName) {
421
- const event = new CustomEvent('viewChange', { detail: { view: viewName } });
422
- document.dispatchEvent(event);
423
- }
424
- }
425
-
426
- // ═══════════════════════════════════════════════════════════════════
427
- // DASHBOARD APPLICATION
428
- // ═══════════════════════════════════════════════════════════════════
429
-
430
- class DashboardApp {
431
- constructor() {
432
- this.ws = new WebSocketClient(CONFIG.WS_URL);
433
- this.api = new APIClient(CONFIG.BACKEND_URL);
434
- this.viewManager = new ViewManager();
435
- this.updateInterval = null;
436
- this.data = {
437
- market: null,
438
- sentiment: null,
439
- trending: null,
440
- news: [],
441
- };
442
- }
443
-
444
- async init() {
445
- console.log('[App] Initializing dashboard...');
446
-
447
- // Connect WebSocket
448
- this.ws.connect();
449
- this.setupWebSocketHandlers();
450
-
451
- // Setup UI handlers
452
- this.setupUIHandlers();
453
-
454
- // Load initial data
455
- await this.loadInitialData();
456
-
457
- // Start periodic updates
458
- this.startPeriodicUpdates();
459
-
460
- console.log('[App] Dashboard initialized successfully');
461
- }
462
-
463
- setupWebSocketHandlers() {
464
- this.ws.on('connected', (isConnected) => {
465
- console.log('[App] WebSocket connection status:', isConnected);
466
- if (isConnected) {
467
- this.ws.send({ type: 'subscribe', groups: ['market', 'sentiment'] });
468
- }
469
- });
470
-
471
- this.ws.on('market_update', (data) => {
472
- console.log('[App] Market update received');
473
- this.handleMarketUpdate(data);
474
- });
475
-
476
- this.ws.on('sentiment_update', (data) => {
477
- console.log('[App] Sentiment update received');
478
- this.handleSentimentUpdate(data);
479
- });
480
-
481
- this.ws.on('stats_update', (data) => {
482
- console.log('[App] Stats update received');
483
- this.updateOnlineUsers(data.active_connections || 0);
484
- });
485
- }
486
-
487
- setupUIHandlers() {
488
- // Theme toggle
489
- const themeToggle = document.getElementById('theme-toggle');
490
- if (themeToggle) {
491
- themeToggle.addEventListener('click', () => this.toggleTheme());
492
- }
493
-
494
- // Notifications
495
- const notificationsBtn = document.getElementById('notifications-btn');
496
- const notificationsPanel = document.getElementById('notifications-panel');
497
- const closeNotifications = document.getElementById('close-notifications');
498
-
499
- if (notificationsBtn && notificationsPanel) {
500
- notificationsBtn.addEventListener('click', () => {
501
- notificationsPanel.classList.toggle('active');
502
- });
503
- }
504
-
505
- if (closeNotifications && notificationsPanel) {
506
- closeNotifications.addEventListener('click', () => {
507
- notificationsPanel.classList.remove('active');
508
- });
509
- }
510
-
511
- // Refresh buttons
512
- const refreshCoins = document.getElementById('refresh-coins');
513
- if (refreshCoins) {
514
- refreshCoins.addEventListener('click', () => this.loadMarketData());
515
- }
516
-
517
- // Floating stats minimize
518
- const minimizeStats = document.getElementById('minimize-stats');
519
- const floatingStats = document.getElementById('floating-stats');
520
- if (minimizeStats && floatingStats) {
521
- minimizeStats.addEventListener('click', () => {
522
- floatingStats.classList.toggle('minimized');
523
- });
524
- }
525
-
526
- // Global search
527
- const globalSearch = document.getElementById('global-search');
528
- if (globalSearch) {
529
- globalSearch.addEventListener('input', Utils.debounce((e) => {
530
- this.handleSearch(e.target.value);
531
- }, 300));
532
- }
533
-
534
- // AI Tools
535
- this.setupAIToolHandlers();
536
- }
537
-
538
- setupAIToolHandlers() {
539
- const sentimentBtn = document.getElementById('sentiment-analysis-btn');
540
- const summaryBtn = document.getElementById('news-summary-btn');
541
- const predictionBtn = document.getElementById('price-prediction-btn');
542
- const patternBtn = document.getElementById('pattern-detection-btn');
543
-
544
- if (sentimentBtn) {
545
- sentimentBtn.addEventListener('click', () => this.runSentimentAnalysis());
546
- }
547
-
548
- if (summaryBtn) {
549
- summaryBtn.addEventListener('click', () => this.runNewsSummary());
550
- }
551
-
552
- if (predictionBtn) {
553
- predictionBtn.addEventListener('click', () => this.runPricePrediction());
554
- }
555
-
556
- if (patternBtn) {
557
- patternBtn.addEventListener('click', () => this.runPatternDetection());
558
- }
559
-
560
- const clearResults = document.getElementById('clear-results');
561
- const aiResults = document.getElementById('ai-results');
562
- if (clearResults && aiResults) {
563
- clearResults.addEventListener('click', () => {
564
- aiResults.style.display = 'none';
565
- });
566
- }
567
- }
568
-
569
- async loadInitialData() {
570
- this.showLoadingOverlay(true);
571
-
572
- try {
573
- await Promise.all([
574
- this.loadMarketData(),
575
- this.loadSentimentData(),
576
- this.loadTrendingData(),
577
- this.loadNewsData(),
578
- ]);
579
- } catch (error) {
580
- console.error('[App] Error loading initial data:', error);
581
- }
582
-
583
- this.showLoadingOverlay(false);
584
- }
585
-
586
- async loadMarketData() {
587
- try {
588
- const data = await this.api.getMarket();
589
- this.data.market = data;
590
- this.renderMarketStats(data);
591
- this.renderCoinsTable(data.cryptocurrencies || []);
592
- } catch (error) {
593
- console.error('[App] Error loading market data:', error);
594
- }
595
- }
596
-
597
- async loadSentimentData() {
598
- try {
599
- const data = await this.api.getSentiment();
600
- this.data.sentiment = data;
601
- this.renderSentiment(data);
602
- } catch (error) {
603
- console.error('[App] Error loading sentiment data:', error);
604
- }
605
- }
606
-
607
- async loadTrendingData() {
608
- try {
609
- const data = await this.api.getTrending();
610
- this.data.trending = data;
611
- } catch (error) {
612
- console.error('[App] Error loading trending data:', error);
613
- }
614
- }
615
-
616
- async loadNewsData() {
617
- try {
618
- const data = await this.api.getNews(20);
619
- this.data.news = data.news || [];
620
- this.renderNews(this.data.news);
621
- } catch (error) {
622
- console.error('[App] Error loading news data:', error);
623
- }
624
- }
625
-
626
- renderMarketStats(data) {
627
- const totalMarketCap = document.getElementById('total-market-cap');
628
- const btcDominance = document.getElementById('btc-dominance');
629
- const volume24h = document.getElementById('volume-24h');
630
-
631
- if (totalMarketCap && data.total_market_cap) {
632
- totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap);
633
- }
634
-
635
- if (btcDominance && data.btc_dominance) {
636
- btcDominance.textContent = `${data.btc_dominance.toFixed(1)}%`;
637
- }
638
-
639
- if (volume24h && data.total_volume_24h) {
640
- volume24h.textContent = Utils.formatCurrency(data.total_volume_24h);
641
- }
642
- }
643
-
644
- renderCoinsTable(coins) {
645
- const tbody = document.getElementById('coins-table-body');
646
- if (!tbody) return;
647
-
648
- if (!coins || coins.length === 0) {
649
- tbody.innerHTML = '<tr><td colspan="7">داده‌ای یافت نشد</td></tr>';
650
- return;
651
- }
652
-
653
- tbody.innerHTML = coins.slice(0, 20).map((coin, index) => `
654
- <tr>
655
- <td>${index + 1}</td>
656
- <td>
657
- <div style="display: flex; align-items: center; gap: 8px;">
658
- <strong>${coin.symbol}</strong>
659
- <span style="color: var(--text-muted); font-size: 0.875rem;">${coin.name}</span>
660
- </div>
661
- </td>
662
- <td style="font-family: var(--font-mono);">${Utils.formatCurrency(coin.current_price)}</td>
663
- <td>
664
- <span class="stat-change ${Utils.getChangeClass(coin.price_change_percentage_24h)}">
665
- ${Utils.formatPercent(coin.price_change_percentage_24h)}
666
- </span>
667
- </td>
668
- <td>${Utils.formatCurrency(coin.total_volume)}</td>
669
- <td>${Utils.formatCurrency(coin.market_cap)}</td>
670
- <td>
671
- <button class="btn-ghost" onclick="app.viewCoinDetails('${coin.symbol}')">
672
- <i class="fas fa-chart-line"></i>
673
- </button>
674
- </td>
675
- </tr>
676
- `).join('');
677
- }
678
-
679
- renderSentiment(data) {
680
- if (!data) return;
681
-
682
- const bullish = data.bullish || 0;
683
- const neutral = data.neutral || 0;
684
- const bearish = data.bearish || 0;
685
-
686
- const bullishPercent = document.getElementById('bullish-percent');
687
- const neutralPercent = document.getElementById('neutral-percent');
688
- const bearishPercent = document.getElementById('bearish-percent');
689
-
690
- if (bullishPercent) bullishPercent.textContent = `${bullish}%`;
691
- if (neutralPercent) neutralPercent.textContent = `${neutral}%`;
692
- if (bearishPercent) bearishPercent.textContent = `${bearish}%`;
693
-
694
- // Update progress bars
695
- const progressBars = document.querySelectorAll('.sentiment-progress-bar');
696
- progressBars.forEach(bar => {
697
- if (bar.classList.contains('bullish')) {
698
- bar.style.width = `${bullish}%`;
699
- } else if (bar.classList.contains('neutral')) {
700
- bar.style.width = `${neutral}%`;
701
- } else if (bar.classList.contains('bearish')) {
702
- bar.style.width = `${bearish}%`;
703
- }
704
- });
705
- }
706
-
707
- renderNews(news) {
708
- const newsGrid = document.getElementById('news-grid');
709
- if (!newsGrid) return;
710
-
711
- if (!news || news.length === 0) {
712
- newsGrid.innerHTML = '<p>خبری یافت نشد</p>';
713
- return;
714
- }
715
-
716
- newsGrid.innerHTML = news.map(item => `
717
- <div class="news-card">
718
- ${item.image ? `<img src="${item.image}" alt="${item.title}" class="news-card-image">` : ''}
719
- <div class="news-card-content">
720
- <h3 class="news-card-title">${item.title}</h3>
721
- <div class="news-card-meta">
722
- <span><i class="fas fa-clock"></i> ${Utils.formatDate(item.published_at || Date.now())}</span>
723
- <span><i class="fas fa-newspaper"></i> ${item.source || 'Unknown'}</span>
724
- </div>
725
- <p class="news-card-excerpt">${item.description || item.summary || ''}</p>
726
- </div>
727
- </div>
728
- `).join('');
729
- }
730
-
731
- handleMarketUpdate(data) {
732
- if (data.data) {
733
- this.renderMarketStats(data.data);
734
- if (data.data.cryptocurrencies) {
735
- this.renderCoinsTable(data.data.cryptocurrencies);
736
- }
737
- }
738
- }
739
-
740
- handleSentimentUpdate(data) {
741
- if (data.data) {
742
- this.renderSentiment(data.data);
743
- }
744
- }
745
-
746
- updateOnlineUsers(count) {
747
- const activeUsersCount = document.getElementById('active-users-count');
748
- if (activeUsersCount) {
749
- activeUsersCount.textContent = count;
750
- }
751
- }
752
-
753
- startPeriodicUpdates() {
754
- this.updateInterval = setInterval(() => {
755
- console.log('[App] Periodic update triggered');
756
- this.loadMarketData();
757
- this.loadSentimentData();
758
- }, CONFIG.UPDATE_INTERVAL);
759
- }
760
-
761
- stopPeriodicUpdates() {
762
- if (this.updateInterval) {
763
- clearInterval(this.updateInterval);
764
- this.updateInterval = null;
765
- }
766
- }
767
-
768
- toggleTheme() {
769
- document.body.classList.toggle('light-theme');
770
- const icon = document.querySelector('#theme-toggle i');
771
- if (icon) {
772
- icon.classList.toggle('fa-moon');
773
- icon.classList.toggle('fa-sun');
774
- }
775
- }
776
-
777
- handleSearch(query) {
778
- console.log('[App] Search query:', query);
779
- // Implement search functionality
780
- }
781
-
782
- viewCoinDetails(symbol) {
783
- console.log('[App] View coin details:', symbol);
784
- // Switch to charts view and load coin data
785
- this.viewManager.switchView('charts');
786
- }
787
-
788
- showLoadingOverlay(show) {
789
- const overlay = document.getElementById('loading-overlay');
790
- if (overlay) {
791
- if (show) {
792
- overlay.classList.add('active');
793
- } else {
794
- overlay.classList.remove('active');
795
- }
796
- }
797
- }
798
-
799
- // AI Tool Methods
800
- async runSentimentAnalysis() {
801
- const aiResults = document.getElementById('ai-results');
802
- const aiResultsContent = document.getElementById('ai-results-content');
803
-
804
- if (!aiResults || !aiResultsContent) return;
805
-
806
- aiResults.style.display = 'block';
807
- aiResultsContent.innerHTML = '<div class="loader"></div> در حال تحلیل...';
808
-
809
- try {
810
- const data = await this.api.getSentiment();
811
-
812
- aiResultsContent.innerHTML = `
813
- <div class="ai-result-card">
814
- <h4>نتایج تحلیل احساسات</h4>
815
- <div class="sentiment-summary">
816
- <div class="sentiment-summary-item">
817
- <div class="summary-label">صعودی</div>
818
- <div class="summary-value bullish">${data.bullish}%</div>
819
- </div>
820
- <div class="sentiment-summary-item">
821
- <div class="summary-label">خنثی</div>
822
- <div class="summary-value neutral">${data.neutral}%</div>
823
- </div>
824
- <div class="sentiment-summary-item">
825
- <div class="summary-label">نزولی</div>
826
- <div class="summary-value bearish">${data.bearish}%</div>
827
- </div>
828
- </div>
829
- <p style="margin-top: 1rem; color: var(--text-muted);">
830
- ${data.summary || 'تحلیل احساسات بازار بر اساس داده‌های جمع‌آوری شده از منابع مختلف'}
831
- </p>
832
- </div>
833
- `;
834
- } catch (error) {
835
- aiResultsContent.innerHTML = `
836
- <div class="error-message">
837
- <i class="fas fa-exclamation-circle"></i>
838
- خطا در تحلیل: ${error.message}
839
- </div>
840
- `;
841
- }
842
- }
843
-
844
- async runNewsSummary() {
845
- const aiResults = document.getElementById('ai-results');
846
- const aiResultsContent = document.getElementById('ai-results-content');
847
-
848
- if (!aiResults || !aiResultsContent) return;
849
-
850
- aiResults.style.display = 'block';
851
- aiResultsContent.innerHTML = '<div class="loader"></div> در حال خلاصه‌سازی...';
852
-
853
- setTimeout(() => {
854
- aiResultsContent.innerHTML = `
855
- <div class="ai-result-card">
856
- <h4>خلاصه اخبار</h4>
857
- <p>قابلیت خلاصه‌سازی اخبار به زودی اضافه خواهد شد.</p>
858
- <p style="color: var(--text-muted); font-size: 0.875rem;">
859
- این قابلیت از مدل‌های Hugging Face برای خلاصه‌سازی متن استفاده می‌کند.
860
- </p>
861
- </div>
862
- `;
863
- }, 1000);
864
- }
865
-
866
- async runPricePrediction() {
867
- const aiResults = document.getElementById('ai-results');
868
- const aiResultsContent = document.getElementById('ai-results-content');
869
-
870
- if (!aiResults || !aiResultsContent) return;
871
-
872
- aiResults.style.display = 'block';
873
- aiResultsContent.innerHTML = '<div class="loader"></div> در حال پیش‌بینی...';
874
-
875
- setTimeout(() => {
876
- aiResultsContent.innerHTML = `
877
- <div class="ai-result-card">
878
- <h4>پیش‌بینی قیمت</h4>
879
- <p>قابلیت پیش‌بینی قیمت به زودی اضافه خواهد شد.</p>
880
- <p style="color: var(--text-muted); font-size: 0.875rem;">
881
- این قابلیت از مدل‌های یادگیری ماشین برای پیش‌بینی روند قیمت استفاده می‌کند.
882
- </p>
883
- </div>
884
- `;
885
- }, 1000);
886
- }
887
-
888
- async runPatternDetection() {
889
- const aiResults = document.getElementById('ai-results');
890
- const aiResultsContent = document.getElementById('ai-results-content');
891
-
892
- if (!aiResults || !aiResultsContent) return;
893
-
894
- aiResults.style.display = 'block';
895
- aiResultsContent.innerHTML = '<div class="loader"></div> در حال تشخیص الگو...';
896
-
897
- setTimeout(() => {
898
- aiResultsContent.innerHTML = `
899
- <div class="ai-result-card">
900
- <h4>تشخیص الگو</h4>
901
- <p>قابلیت تشخیص الگو به زودی اضافه خواهد شد.</p>
902
- <p style="color: var(--text-muted); font-size: 0.875rem;">
903
- این قابلیت الگوهای کندل استیک و تحلیل تکنیکال را شناسایی می‌کند.
904
- </p>
905
- </div>
906
- `;
907
- }, 1000);
908
- }
909
-
910
- destroy() {
911
- this.stopPeriodicUpdates();
912
- this.ws.disconnect();
913
- console.log('[App] Dashboard destroyed');
914
- }
915
- }
916
-
917
- // ═══════════════════════════════════════════════════════════════════
918
- // INITIALIZATION
919
- // ═══════════════════════════════════════════════════════════════════
920
-
921
- let app;
922
-
923
- document.addEventListener('DOMContentLoaded', () => {
924
- console.log('[Main] DOM loaded, initializing application...');
925
-
926
- app = new DashboardApp();
927
- app.init();
928
-
929
- // Make app globally accessible for debugging
930
- window.app = app;
931
-
932
- console.log('[Main] Application ready');
933
- });
934
-
935
- // Cleanup on page unload
936
- window.addEventListener('beforeunload', () => {
937
- if (app) {
938
- app.destroy();
939
- }
940
- });
941
-
942
- // Handle visibility change to pause/resume updates
943
- document.addEventListener('visibilitychange', () => {
944
- if (document.hidden) {
945
- console.log('[Main] Page hidden, pausing updates');
946
- app.stopPeriodicUpdates();
947
- } else {
948
- console.log('[Main] Page visible, resuming updates');
949
- app.startPeriodicUpdates();
950
- app.loadMarketData();
951
- }
952
- });
953
-
954
- // Export for module usage
955
- export { DashboardApp, APIClient, WebSocketClient, Utils };
 
1
+ import apiClient from './apiClient.js';
2
+ import wsClient from './wsClient.js';
3
+ import OverviewView from './overviewView.js';
4
+ import MarketView from './marketView.js';
5
+ import NewsView from './newsView.js';
6
+ import ChartLabView from './chartLabView.js';
7
+ import AIAdvisorView from './aiAdvisorView.js';
8
+ import DatasetsModelsView from './datasetsModelsView.js';
9
+ import DebugConsoleView from './debugConsoleView.js';
10
+ import SettingsView from './settingsView.js';
11
+ import ProvidersView from './providersView.js';
12
+ import ApiExplorerView from './apiExplorerView.js';
13
+
14
+ const App = {
15
+ init() {
16
+ this.cacheElements();
17
+ this.bindNavigation();
18
+ this.initViews();
19
+ this.initStatusBadges();
20
+ wsClient.connect();
21
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ cacheElements() {
24
+ this.sections = document.querySelectorAll('.page');
25
+ this.navButtons = document.querySelectorAll('[data-nav]');
26
+ this.apiHealthBadge = document.querySelector('[data-api-health]');
27
+ this.wsBadge = document.querySelector('[data-ws-status]');
28
+ },
 
 
29
 
30
+ bindNavigation() {
31
+ this.navButtons.forEach((button) => {
32
+ button.addEventListener('click', () => {
33
+ const target = button.dataset.nav;
34
+ this.sections.forEach((section) => section.classList.toggle('active', section.id === target));
35
+ this.navButtons.forEach((btn) => btn.classList.toggle('active', btn === button));
 
 
 
36
  });
37
+ });
38
+ },
39
 
40
+ initViews() {
41
+ const overview = new OverviewView(document.getElementById('page-overview'));
42
+ overview.init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ const market = new MarketView(document.getElementById('page-market'), wsClient);
45
+ market.init();
 
 
46
 
47
+ const news = new NewsView(document.getElementById('page-news'));
48
+ news.init();
 
49
 
50
+ const chartLab = new ChartLabView(document.getElementById('page-chart'));
51
+ chartLab.init();
 
52
 
53
+ const aiAdvisor = new AIAdvisorView(document.getElementById('page-ai'));
54
+ aiAdvisor.init();
 
55
 
56
+ const datasets = new DatasetsModelsView(document.getElementById('page-datasets'));
57
+ datasets.init();
 
 
58
 
59
+ const debugView = new DebugConsoleView(document.getElementById('page-debug'), wsClient);
60
+ debugView.init();
 
 
61
 
62
+ const settings = new SettingsView(document.getElementById('page-settings'));
63
+ settings.init();
 
 
 
64
 
65
+ const providersView = new ProvidersView(document.getElementById('page-providers'));
66
+ providersView.init();
 
67
 
68
+ const apiExplorer = new ApiExplorerView(document.getElementById('page-api'));
69
+ apiExplorer.init();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  },
71
 
72
+ initStatusBadges() {
73
+ this.refreshHealth();
74
+ wsClient.onStatusChange((status) => {
75
+ if (!this.wsBadge) return;
76
+ const state = status === 'connected' ? 'ok' : status === 'connecting' ? 'warn' : 'error';
77
+ this.wsBadge.dataset.state = state;
78
+ const textNode = this.wsBadge.querySelectorAll('span')[1];
79
+ if (textNode) textNode.textContent = status;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  });
81
  },
82
 
83
+ async refreshHealth() {
84
+ if (!this.apiHealthBadge) return;
85
+ const result = await apiClient.getHealth();
86
+ if (result.ok) {
87
+ this.apiHealthBadge.dataset.state = 'ok';
88
+ const textNode = this.apiHealthBadge.querySelectorAll('span')[1];
89
+ if (textNode) textNode.textContent = result.data?.status || 'healthy';
90
+ } else {
91
+ this.apiHealthBadge.dataset.state = 'error';
92
+ const textNode = this.apiHealthBadge.querySelectorAll('span')[1];
93
+ if (textNode) textNode.textContent = 'error';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
95
  },
 
 
 
 
 
 
 
 
 
 
 
 
96
  };
97
 
98
+ window.addEventListener('DOMContentLoaded', () => App.init());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/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,15 +1,8 @@
1
  export function formatCurrency(value) {
2
- if (value === null || value === undefined || value === '') {
3
  return '—';
4
  }
5
  const num = Number(value);
6
- if (Number.isNaN(num)) {
7
- return '—';
8
- }
9
- // Don't return '—' for 0, show $0.00 instead
10
- if (num === 0) {
11
- return '$0.00';
12
- }
13
  if (Math.abs(num) >= 1_000_000_000_000) {
14
  return `$${(num / 1_000_000_000_000).toFixed(2)}T`;
15
  }
@@ -19,10 +12,7 @@ export function formatCurrency(value) {
19
  if (Math.abs(num) >= 1_000_000) {
20
  return `$${(num / 1_000_000).toFixed(2)}M`;
21
  }
22
- if (Math.abs(num) >= 1_000) {
23
- return `$${(num / 1_000).toFixed(2)}K`;
24
- }
25
- return `$${num.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`;
26
  }
27
 
28
  export function formatPercent(value) {
 
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
  }
 
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) {
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,83 +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 environment
23
- * Uses localhost for local development, HuggingFace Space URL for production
24
- */
25
  get url() {
26
- if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
27
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
28
- return `${protocol}//${window.location.hostname}:7860/ws`;
29
- } else {
30
- return 'wss://really-amin-datasourceforcryptocurrency.hf.space/ws';
31
- }
32
  }
33
 
34
- /**
35
- * Log WebSocket events for debugging and monitoring
36
- * Maintains a rolling buffer of the last 100 events
37
- * @param {Object} event - Event object to log
38
- */
39
  logEvent(event) {
40
- const entry = {
41
- ...event,
42
- time: new Date().toISOString(),
43
- attempt: this.reconnectAttempts
44
- };
45
  this.eventLog.push(entry);
46
- // Keep only last 100 events
47
- if (this.eventLog.length > 100) {
48
- this.eventLog = this.eventLog.slice(-100);
49
- }
50
- console.log('[WSClient]', entry);
51
  }
52
 
53
- /**
54
- * Subscribe to connection status changes
55
- * @param {Function} callback - Called with new status ('connecting', 'connected', 'disconnected', 'error')
56
- * @returns {Function} Unsubscribe function
57
- */
58
  onStatusChange(callback) {
59
- if (typeof callback !== 'function') {
60
- throw new Error('Callback must be a function');
61
- }
62
  this.statusSubscribers.add(callback);
63
- // Immediately call with current status
64
  callback(this.status);
65
  return () => this.statusSubscribers.delete(callback);
66
  }
67
 
68
- /**
69
- * Subscribe to all WebSocket messages
70
- * @param {Function} callback - Called with parsed message data
71
- * @returns {Function} Unsubscribe function
72
- */
73
  onMessage(callback) {
74
- if (typeof callback !== 'function') {
75
- throw new Error('Callback must be a function');
76
- }
77
  this.globalSubscribers.add(callback);
78
  return () => this.globalSubscribers.delete(callback);
79
  }
80
 
81
- /**
82
- * Subscribe to specific message types
83
- * @param {string} type - Message type to subscribe to (e.g., 'market_update', 'news_update')
84
- * @param {Function} callback - Called with messages of the specified type
85
- * @returns {Function} Unsubscribe function
86
- */
87
  subscribe(type, callback) {
88
- if (typeof callback !== 'function') {
89
- throw new Error('Callback must be a function');
90
- }
91
  if (!this.typeSubscribers.has(type)) {
92
  this.typeSubscribers.set(type, new Set());
93
  }
@@ -96,269 +43,69 @@ class WSClient {
96
  return () => set.delete(callback);
97
  }
98
 
99
- /**
100
- * Update connection status and notify all subscribers
101
- * @param {string} newStatus - New status value
102
- */
103
  updateStatus(newStatus) {
104
- if (this.status !== newStatus) {
105
- const oldStatus = this.status;
106
- this.status = newStatus;
107
- this.logEvent({
108
- type: 'status_change',
109
- from: oldStatus,
110
- to: newStatus
111
- });
112
- this.statusSubscribers.forEach(cb => {
113
- try {
114
- cb(newStatus);
115
- } catch (error) {
116
- console.error('[WSClient] Error in status subscriber:', error);
117
- }
118
- });
119
- }
120
  }
121
 
122
- /**
123
- * Establish WebSocket connection with automatic reconnection
124
- * Implements exponential backoff for reconnection attempts
125
- */
126
  connect() {
127
- // Prevent multiple simultaneous connection attempts
128
- if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) {
129
- console.log('[WSClient] Already connected or connecting');
130
  return;
131
  }
132
 
133
- this.connectionStartTime = Date.now();
134
  this.updateStatus('connecting');
135
-
136
- try {
137
- this.socket = new WebSocket(this.url);
138
- this.logEvent({
139
- type: 'connection_attempt',
140
- url: this.url,
141
- attempt: this.reconnectAttempts + 1
142
- });
143
-
144
- this.socket.onopen = () => {
145
- const connectionTime = Date.now() - this.connectionStartTime;
146
- this.backoff = 1000; // Reset backoff on successful connection
147
- this.reconnectAttempts = 0;
148
- this.updateStatus('connected');
149
- this.logEvent({
150
- type: 'connection_established',
151
- connectionTime: `${connectionTime}ms`
152
- });
153
- console.log(`[WSClient] Connected to ${this.url} in ${connectionTime}ms`);
154
- };
155
-
156
- this.socket.onmessage = (event) => {
157
- try {
158
- const data = JSON.parse(event.data);
159
- this.logEvent({
160
- type: 'message_received',
161
- messageType: data.type || 'unknown',
162
- size: event.data.length
163
- });
164
-
165
- // Notify global subscribers
166
- this.globalSubscribers.forEach(cb => {
167
- try {
168
- cb(data);
169
- } catch (error) {
170
- console.error('[WSClient] Error in global subscriber:', error);
171
- }
172
- });
173
-
174
- // Notify type-specific subscribers
175
- if (data.type && this.typeSubscribers.has(data.type)) {
176
- this.typeSubscribers.get(data.type).forEach(cb => {
177
- try {
178
- cb(data);
179
- } catch (error) {
180
- console.error(`[WSClient] Error in ${data.type} subscriber:`, error);
181
- }
182
- });
183
- }
184
- } catch (error) {
185
- console.error('[WSClient] Message parse error:', error);
186
- this.logEvent({
187
- type: 'parse_error',
188
- error: error.message,
189
- rawData: event.data.substring(0, 100)
190
- });
191
- }
192
- };
193
-
194
- this.socket.onclose = (event) => {
195
- const wasConnected = this.status === 'connected';
196
- this.updateStatus('disconnected');
197
- this.logEvent({
198
- type: 'connection_closed',
199
- code: event.code,
200
- reason: event.reason || 'No reason provided',
201
- wasClean: event.wasClean
202
- });
203
-
204
- // Attempt reconnection if enabled
205
- if (this.shouldReconnect) {
206
- this.reconnectAttempts++;
207
- const delay = this.backoff;
208
- this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
209
-
210
- console.log(`[WSClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`);
211
- this.logEvent({
212
- type: 'reconnect_scheduled',
213
- delay: `${delay}ms`,
214
- nextBackoff: `${this.backoff}ms`
215
- });
216
-
217
- setTimeout(() => this.connect(), delay);
218
  }
219
- };
 
 
 
220
 
221
- this.socket.onerror = (error) => {
222
- console.error('[WSClient] WebSocket error:', error);
223
- this.updateStatus('error');
224
- this.logEvent({
225
- type: 'connection_error',
226
- error: error.message || 'Unknown error',
227
- readyState: this.socket ? this.socket.readyState : 'null'
228
- });
229
- };
230
- } catch (error) {
231
- console.error('[WSClient] Failed to create WebSocket:', error);
232
- this.updateStatus('error');
233
- this.logEvent({
234
- type: 'creation_error',
235
- error: error.message
236
- });
237
-
238
- // Retry connection if enabled
239
  if (this.shouldReconnect) {
240
- this.reconnectAttempts++;
241
  const delay = this.backoff;
242
  this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
243
  setTimeout(() => this.connect(), delay);
244
  }
245
- }
 
 
 
 
 
 
 
 
246
  }
247
 
248
- /**
249
- * Gracefully disconnect WebSocket and disable automatic reconnection
250
- */
251
  disconnect() {
252
  this.shouldReconnect = false;
253
  if (this.socket) {
254
- this.logEvent({ type: 'manual_disconnect' });
255
- this.socket.close(1000, 'Client disconnect');
256
- this.socket = null;
257
  }
258
  }
259
 
260
- /**
261
- * Manually trigger reconnection (useful for testing or recovery)
262
- */
263
- reconnect() {
264
- this.disconnect();
265
- this.shouldReconnect = true;
266
- this.backoff = 1000; // Reset backoff
267
- this.reconnectAttempts = 0;
268
- this.connect();
269
- }
270
-
271
- /**
272
- * Send a message through the WebSocket connection
273
- * @param {Object} data - Data to send (will be JSON stringified)
274
- * @returns {boolean} True if sent successfully, false otherwise
275
- */
276
- send(data) {
277
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
278
- console.error('[WSClient] Cannot send message: not connected');
279
- this.logEvent({
280
- type: 'send_failed',
281
- reason: 'not_connected',
282
- readyState: this.socket ? this.socket.readyState : 'null'
283
- });
284
- return false;
285
- }
286
-
287
- try {
288
- const message = JSON.stringify(data);
289
- this.socket.send(message);
290
- this.logEvent({
291
- type: 'message_sent',
292
- messageType: data.type || 'unknown',
293
- size: message.length
294
- });
295
- return true;
296
- } catch (error) {
297
- console.error('[WSClient] Failed to send message:', error);
298
- this.logEvent({
299
- type: 'send_error',
300
- error: error.message
301
- });
302
- return false;
303
- }
304
- }
305
-
306
- /**
307
- * Get a copy of the event log
308
- * @returns {Array} Array of logged events
309
- */
310
  getEvents() {
311
  return [...this.eventLog];
312
  }
313
-
314
- /**
315
- * Get current connection statistics
316
- * @returns {Object} Connection statistics
317
- */
318
- getStats() {
319
- return {
320
- status: this.status,
321
- reconnectAttempts: this.reconnectAttempts,
322
- currentBackoff: this.backoff,
323
- maxBackoff: this.maxBackoff,
324
- shouldReconnect: this.shouldReconnect,
325
- subscriberCounts: {
326
- status: this.statusSubscribers.size,
327
- global: this.globalSubscribers.size,
328
- typed: Array.from(this.typeSubscribers.entries()).map(([type, subs]) => ({
329
- type,
330
- count: subs.size
331
- }))
332
- },
333
- eventLogSize: this.eventLog.length,
334
- url: this.url
335
- };
336
- }
337
-
338
- /**
339
- * Check if WebSocket is currently connected
340
- * @returns {boolean} True if connected
341
- */
342
- isConnected() {
343
- return this.socket && this.socket.readyState === WebSocket.OPEN;
344
- }
345
-
346
- /**
347
- * Clear all subscribers (useful for cleanup)
348
- */
349
- clearSubscribers() {
350
- this.statusSubscribers.clear();
351
- this.globalSubscribers.clear();
352
- this.typeSubscribers.clear();
353
- this.logEvent({ type: 'subscribers_cleared' });
354
- }
355
  }
356
 
357
- // Create singleton instance
358
  const wsClient = new WSClient();
359
-
360
- // Auto-connect on module load
361
- wsClient.connect();
362
-
363
- // Export singleton instance
364
- 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;
 
 
 
 
 
unified_dashboard.html CHANGED
@@ -8,152 +8,54 @@
8
  <link rel="stylesheet" href="static/css/design-system.css" />
9
  <link rel="stylesheet" href="static/css/dashboard.css" />
10
  <link rel="stylesheet" href="static/css/pro-dashboard.css" />
11
- <link rel="stylesheet" href="static/css/modern-dashboard.css" />
12
- <link rel="stylesheet" href="static/css/glassmorphism.css" />
13
- <link rel="stylesheet" href="static/css/light-minimal-theme.css" />
14
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
15
- <script src="static/js/animations.js" defer></script>
16
- <script src="static/js/menu-system.js" defer></script>
17
- <script src="static/js/huggingface-integration.js" defer></script>
18
  </head>
19
- <body data-theme="light">
20
  <div class="app-shell">
21
- <aside class="sidebar sidebar-modern">
22
- <div class="brand brand-modern">
23
- <div class="brand-icon">
24
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 
25
  <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
26
  <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
27
  <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
28
  </svg>
29
- </div>
30
- <div class="brand-text">
31
- <strong>Crypto Monitor HF</strong>
32
- <span class="env-pill">
33
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
34
- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
35
- </svg>
36
- HF Space
37
- </span>
38
- </div>
39
  </div>
40
- <nav class="nav nav-modern">
41
- <button class="nav-button nav-button-modern active" data-nav="page-overview">
42
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
43
- <path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
44
- <path d="M9 22V12h6v10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
45
- </svg>
46
- <span>Overview</span>
47
- </button>
48
- <button class="nav-button nav-button-modern" data-nav="page-market">
49
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
50
- <path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
51
- <path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
52
- </svg>
53
- <span>Market</span>
54
- </button>
55
- <button class="nav-button nav-button-modern" data-nav="page-chart">
56
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
57
- <path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
58
- <path d="M7 16l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
59
- </svg>
60
- <span>Chart Lab</span>
61
- </button>
62
- <button class="nav-button nav-button-modern" data-nav="page-ai">
63
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
64
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
65
- </svg>
66
- <span>Sentiment & AI</span>
67
- </button>
68
- <button class="nav-button nav-button-modern" data-nav="page-news">
69
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
70
- <path d="M4 19.5A2.5 2.5 0 016.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
71
- <path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
72
- </svg>
73
- <span>News</span>
74
- </button>
75
- <button class="nav-button nav-button-modern" data-nav="page-providers">
76
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
77
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
78
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
79
- </svg>
80
- <span>Providers</span>
81
- </button>
82
- <button class="nav-button nav-button-modern" data-nav="page-api">
83
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
84
- <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
85
- </svg>
86
- <span>API Explorer</span>
87
- </button>
88
- <button class="nav-button nav-button-modern" data-nav="page-debug">
89
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
90
- <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
91
- <path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
92
- </svg>
93
- <span>Diagnostics</span>
94
- </button>
95
- <button class="nav-button nav-button-modern" data-nav="page-datasets">
96
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
97
- <path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
98
- </svg>
99
- <span>Datasets & Models</span>
100
- </button>
101
- <button class="nav-button nav-button-modern" data-nav="page-settings">
102
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
103
- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
104
- <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
105
- </svg>
106
- <span>Settings</span>
107
- </button>
108
  </nav>
109
  <div class="sidebar-footer">
110
- <div class="footer-badge">
111
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
112
- <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
113
- </svg>
114
- Unified Intelligence Console
115
- </div>
116
  </div>
117
  </aside>
118
  <main class="main-area">
119
- <header class="modern-header">
120
- <div style="display: flex; align-items: center; justify-content: space-between; width: 100%; flex-wrap: wrap; gap: 16px;">
121
- <div>
122
- <h1 style="margin: 0; font-size: 1.75rem; font-weight: 800; background: linear-gradient(135deg, #00D4FF, #8B5CF6, #EC4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">Unified Intelligence Dashboard</h1>
123
- <p class="text-muted" style="margin: 4px 0 0 0; font-size: 0.875rem;">Live market telemetry, AI signals, diagnostics, and provider health.</p>
 
 
 
 
124
  </div>
125
- <div class="status-group" style="display: flex; gap: 12px; align-items: center; position: relative;">
126
- <div class="status-pill" data-api-health data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
127
- <span class="status-dot"></span>
128
- <span>checking</span>
129
- </div>
130
- <div class="status-pill" data-ws-status data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
131
- <span class="status-dot"></span>
132
- <span>connecting</span>
133
- </div>
134
- <button class="button-3d" data-menu-trigger="theme-menu" style="padding: 8px 16px; position: relative;">
135
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
136
- <circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
137
- <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
138
- </svg>
139
- </button>
140
- <div class="menu-dropdown" data-menu="theme-menu" style="display: none; top: 100%; right: 0; margin-top: 8px;">
141
- <div class="menu-item" data-action="theme-light">
142
- <span>☀️ Light Theme</span>
143
- </div>
144
- <div class="menu-item" data-action="theme-dark">
145
- <span>🌙 Dark Theme</span>
146
- </div>
147
- <div class="menu-separator"></div>
148
- <div class="menu-item" data-action="settings">
149
- <span>⚙️ Settings</span>
150
- </div>
151
- </div>
152
  </div>
153
  </div>
154
- <div class="header-crypto-list" data-header-crypto-list style="margin-top: 16px; width: 100%;">
155
- <!-- Crypto list will be populated by JavaScript -->
156
- </div>
157
  </header>
158
  <div class="page-container">
159
  <section id="page-overview" class="page active">
@@ -193,30 +95,6 @@
193
  <canvas id="sentiment-chart" height="220"></canvas>
194
  </div>
195
  </div>
196
- <div class="glass-card" style="margin-top: 24px;">
197
- <div class="section-header">
198
- <h3>Backend Information</h3>
199
- <span class="text-muted">System Status</span>
200
- </div>
201
- <div data-backend-info class="backend-info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px;">
202
- <div class="backend-info-item">
203
- <span class="info-label">API Status</span>
204
- <span class="info-value" data-api-status>Checking...</span>
205
- </div>
206
- <div class="backend-info-item">
207
- <span class="info-label">WebSocket</span>
208
- <span class="info-value" data-ws-status>Connecting...</span>
209
- </div>
210
- <div class="backend-info-item">
211
- <span class="info-label">Providers</span>
212
- <span class="info-value" data-providers-count>—</span>
213
- </div>
214
- <div class="backend-info-item">
215
- <span class="info-label">Last Update</span>
216
- <span class="info-value" data-last-update>—</span>
217
- </div>
218
- </div>
219
- </div>
220
  </section>
221
 
222
  <section id="page-market" class="page">
@@ -275,53 +153,32 @@
275
 
276
  <section id="page-chart" class="page">
277
  <div class="section-header">
278
- <h2 class="section-title">Chart Lab - TradingView Style</h2>
279
  <div class="controls-bar">
280
- <select data-chart-symbol style="padding: 8px 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: var(--text-primary);">
281
  <option value="BTC">BTC</option>
282
  <option value="ETH">ETH</option>
283
  <option value="SOL">SOL</option>
284
  <option value="BNB">BNB</option>
285
- <option value="ADA">ADA</option>
286
- <option value="DOT">DOT</option>
287
- <option value="MATIC">MATIC</option>
288
- <option value="AVAX">AVAX</option>
289
  </select>
290
- <div class="chart-toolbar">
291
- <button class="chart-timeframe-btn active" data-chart-timeframe="1d">1D</button>
292
- <button class="chart-timeframe-btn" data-chart-timeframe="7d">7D</button>
293
- <button class="chart-timeframe-btn" data-chart-timeframe="30d">30D</button>
294
- <button class="chart-timeframe-btn" data-chart-timeframe="90d">90D</button>
295
  </div>
296
  </div>
297
  </div>
298
- <div class="tradingview-chart-container glass-vibrant">
299
- <div class="chart-toolbar">
300
- <div class="chart-indicators">
301
- <label class="chart-indicator-toggle">
302
- <input type="checkbox" data-indicator="MA20" checked />
303
- <span>MA 20</span>
304
- </label>
305
- <label class="chart-indicator-toggle">
306
- <input type="checkbox" data-indicator="MA50" />
307
- <span>MA 50</span>
308
- </label>
309
- <label class="chart-indicator-toggle">
310
- <input type="checkbox" data-indicator="RSI" />
311
- <span>RSI</span>
312
- </label>
313
- <label class="chart-indicator-toggle">
314
- <input type="checkbox" data-indicator="Volume" checked />
315
- <span>Volume</span>
316
- </label>
317
- </div>
318
- </div>
319
- <canvas id="chart-lab-canvas" height="400"></canvas>
320
  </div>
321
- <div class="glass-card glass-vibrant" style="margin-top: 24px;">
322
- <div class="controls-bar" style="margin-bottom: 16px;">
323
- <button class="primary" data-run-analysis style="background: linear-gradient(135deg, #00D4FF, #8B5CF6);">Analyze Chart with AI</button>
 
 
 
324
  </div>
 
325
  <div data-ai-insights class="ai-insights"></div>
326
  </div>
327
  </section>
 
8
  <link rel="stylesheet" href="static/css/design-system.css" />
9
  <link rel="stylesheet" href="static/css/dashboard.css" />
10
  <link rel="stylesheet" href="static/css/pro-dashboard.css" />
 
 
 
11
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
 
 
 
12
  </head>
13
+ <body data-theme="dark">
14
  <div class="app-shell">
15
+ <aside class="sidebar">
16
+ <div class="brand">
17
+ <strong>Crypto Monitor HF</strong>
18
+ <span class="env-pill">
19
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
20
  <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
21
  <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
22
  <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
23
  </svg>
24
+ HF Space
25
+ </span>
 
 
 
 
 
 
 
 
26
  </div>
27
+ <nav class="nav">
28
+ <button class="nav-button active" data-nav="page-overview">Overview</button>
29
+ <button class="nav-button" data-nav="page-market">Market</button>
30
+ <button class="nav-button" data-nav="page-chart">Chart Lab</button>
31
+ <button class="nav-button" data-nav="page-ai">Sentiment & AI</button>
32
+ <button class="nav-button" data-nav="page-news">News</button>
33
+ <button class="nav-button" data-nav="page-providers">Providers</button>
34
+ <button class="nav-button" data-nav="page-api">API Explorer</button>
35
+ <button class="nav-button" data-nav="page-debug">Diagnostics</button>
36
+ <button class="nav-button" data-nav="page-datasets">Datasets & Models</button>
37
+ <button class="nav-button" data-nav="page-settings">Settings</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </nav>
39
  <div class="sidebar-footer">
40
+ Unified crypto intelligence console<br />Realtime data • HF optimized
 
 
 
 
 
41
  </div>
42
  </aside>
43
  <main class="main-area">
44
+ <header class="topbar">
45
+ <div>
46
+ <h1>Unified Intelligence Dashboard</h1>
47
+ <p class="text-muted">Live market telemetry, AI signals, diagnostics, and provider health.</p>
48
+ </div>
49
+ <div class="status-group">
50
+ <div class="status-pill" data-api-health data-state="warn">
51
+ <span class="status-dot"></span>
52
+ <span>checking</span>
53
  </div>
54
+ <div class="status-pill" data-ws-status data-state="warn">
55
+ <span class="status-dot"></span>
56
+ <span>connecting</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  </div>
58
  </div>
 
 
 
59
  </header>
60
  <div class="page-container">
61
  <section id="page-overview" class="page active">
 
95
  <canvas id="sentiment-chart" height="220"></canvas>
96
  </div>
97
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  </section>
99
 
100
  <section id="page-market" class="page">
 
153
 
154
  <section id="page-chart" class="page">
155
  <div class="section-header">
156
+ <h2 class="section-title">Chart Lab</h2>
157
  <div class="controls-bar">
158
+ <select data-chart-symbol>
159
  <option value="BTC">BTC</option>
160
  <option value="ETH">ETH</option>
161
  <option value="SOL">SOL</option>
162
  <option value="BNB">BNB</option>
 
 
 
 
163
  </select>
164
+ <div class="input-chip">
165
+ <button class="ghost active" data-chart-timeframe="7d">7D</button>
166
+ <button class="ghost" data-chart-timeframe="30d">30D</button>
167
+ <button class="ghost" data-chart-timeframe="90d">90D</button>
 
168
  </div>
169
  </div>
170
  </div>
171
+ <div class="glass-card">
172
+ <canvas id="chart-lab-canvas" height="260"></canvas>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  </div>
174
+ <div class="glass-card">
175
+ <div class="controls-bar">
176
+ <label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
177
+ <label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
178
+ <label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
179
+ <label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
180
  </div>
181
+ <button class="primary" data-run-analysis>Analyze Chart with AI</button>
182
  <div data-ai-insights class="ai-insights"></div>
183
  </div>
184
  </section>