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

Upload 303 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +20 -0
  2. Dockerfile +37 -10
  3. TEST_ENDPOINTS.sh +161 -88
  4. admin.html +69 -1007
  5. ai_models.py +157 -38
  6. api-resources/crypto_resources_unified_2025-11-11.json +0 -0
  7. app.js +34 -1
  8. app.py +31 -0
  9. archive_html/admin_advanced.html +1862 -0
  10. archive_html/admin_improved.html +61 -0
  11. archive_html/admin_pro.html +657 -0
  12. archive_html/complete_dashboard.html +857 -0
  13. archive_html/crypto_dashboard_pro.html +441 -0
  14. archive_html/dashboard.html +113 -0
  15. archive_html/dashboard_standalone.html +410 -0
  16. archive_html/enhanced_dashboard.html +876 -0
  17. archive_html/feature_flags_demo.html +393 -0
  18. archive_html/hf_console.html +97 -0
  19. archive_html/improved_dashboard.html +443 -0
  20. archive_html/index (1).html +0 -0
  21. archive_html/index_backup.html +2452 -0
  22. archive_html/index_enhanced.html +2132 -0
  23. archive_html/pool_management.html +765 -0
  24. archive_html/simple_overview.html +303 -0
  25. archive_html/unified_dashboard.html +639 -0
  26. backend/routers/__pycache__/__init__.cpython-313.pyc +0 -0
  27. backend/routers/__pycache__/hf_connect.cpython-313.pyc +0 -0
  28. backend/services/__pycache__/hf_client.cpython-313.pyc +0 -0
  29. backend/services/__pycache__/hf_registry.cpython-313.pyc +0 -0
  30. backend/services/__pycache__/local_resource_service.cpython-313.pyc +0 -0
  31. backend/services/auto_discovery_service.py +4 -1
  32. backend/services/diagnostics_service.py +8 -1
  33. backend/services/hf_registry.py +68 -45
  34. backend/services/local_resource_service.py +207 -0
  35. check_server.py +101 -0
  36. collectors/__pycache__/aggregator.cpython-313.pyc +0 -0
  37. collectors/aggregator.py +113 -4
  38. config.js +372 -129
  39. config.py +10 -0
  40. crypto_resources_unified_2025-11-11.json +0 -0
  41. enhanced_server.py +3 -3
  42. hf_unified_server.py +1055 -113
  43. index.html +0 -0
  44. main.py +29 -17
  45. package-lock.json +908 -1
  46. package.json +13 -1
  47. production_server.py +1 -1
  48. provider_validator.py +7 -1
  49. pytest.ini +4 -0
  50. real_server.py +1 -1
.env ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Configuration
2
+ HUGGINGFACE_TOKEN=your_token_here
3
+ ENABLE_SENTIMENT=true
4
+ SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
5
+ SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
6
+ HF_REGISTRY_REFRESH_SEC=21600
7
+ HF_HTTP_TIMEOUT=8.0
8
+
9
+ # Existing API Keys (if any)
10
+ ETHERSCAN_KEY_1=
11
+ ETHERSCAN_KEY_2=
12
+ BSCSCAN_KEY=
13
+ TRONSCAN_KEY=
14
+ COINMARKETCAP_KEY_1=
15
+ COINMARKETCAP_KEY_2=
16
+ NEWSAPI_KEY=
17
+ CRYPTOCOMPARE_KEY=
18
+
19
+ # HuggingFace API Token
20
+ HF_TOKEN=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
Dockerfile CHANGED
@@ -1,24 +1,51 @@
1
- FROM python:3.10
2
 
3
  WORKDIR /app
4
 
5
- # Create required directories
6
- RUN mkdir -p /app/logs /app/data /app/data/database /app/data/backups
 
 
 
 
7
 
8
- # Copy requirements and install dependencies
9
  COPY requirements.txt .
 
 
 
 
 
10
  RUN pip install --no-cache-dir -r requirements.txt
11
 
12
  # Copy application code
13
  COPY . .
14
 
15
- # Set environment variables
16
- ENV USE_MOCK_DATA=false
17
- ENV PORT=7860
18
- ENV PYTHONUNBUFFERED=1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  # Expose port
21
  EXPOSE 7860
22
 
23
- # Launch command
24
- CMD ["uvicorn", "api_server_extended:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
1
+ FROM python:3.10-slim
2
 
3
  WORKDIR /app
4
 
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ git \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
 
12
+ # Copy requirements first for better caching
13
  COPY requirements.txt .
14
+
15
+ # Upgrade pip
16
+ RUN pip install --no-cache-dir --upgrade pip
17
+
18
+ # Install dependencies
19
  RUN pip install --no-cache-dir -r requirements.txt
20
 
21
  # Copy application code
22
  COPY . .
23
 
24
+ # Create necessary directories
25
+ RUN mkdir -p \
26
+ data/database \
27
+ data/backups \
28
+ logs \
29
+ static/css \
30
+ static/js \
31
+ .cache/huggingface
32
+
33
+ # Set permissions
34
+ RUN chmod -R 755 /app
35
+
36
+ # Environment variables
37
+ ENV PORT=7860 \
38
+ PYTHONUNBUFFERED=1 \
39
+ TRANSFORMERS_CACHE=/app/.cache/huggingface \
40
+ HF_HOME=/app/.cache/huggingface \
41
+ PYTHONDONTWRITEBYTECODE=1
42
 
43
  # Expose port
44
  EXPOSE 7860
45
 
46
+ # Health check
47
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
48
+ CMD curl -f http://localhost:7860/api/health || exit 1
49
+
50
+ # Run application
51
+ CMD ["uvicorn", "hf_unified_server:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
TEST_ENDPOINTS.sh CHANGED
@@ -1,88 +1,161 @@
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"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "======================================"
admin.html CHANGED
@@ -1,1017 +1,79 @@
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>
 
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" />
6
+ <title>Crypto Intelligence Admin</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
+ <link rel="stylesheet" href="/static/css/components.css" />
12
+ <script defer src="/static/js/ui-feedback.js"></script>
13
+ <script defer src="/static/js/admin-app.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  </head>
15
+ <body class="page page-admin">
16
+ <header class="top-nav">
17
+ <div class="branding">
18
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12h16M12 4v16"/></svg>
19
+ <div>
20
+ <strong>Providers & Scheduling</strong>
21
+ <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/providers · /api/logs</small>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </div>
23
+ </div>
24
+ <nav class="nav-links">
25
+ <a href="/dashboard">Dashboard</a>
26
+ <a class="active" href="/admin">Admin</a>
27
+ <a href="/hf_console">HF Console</a>
28
+ <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
+ </nav>
30
+ </header>
31
+
32
+ <main class="page-content">
33
+ <section class="card">
34
+ <div class="section-heading">
35
+ <h2>Providers Health</h2>
36
+ <span class="badge info" id="providers-count">Loading...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
+ <div class="table-card">
39
+ <table>
40
+ <thead>
41
+ <tr><th>Provider</th><th>Status</th><th>Response (ms)</th><th>Category</th></tr>
42
+ </thead>
43
+ <tbody id="providers-table">
44
+ <tr><td colspan="4">Loading providers...</td></tr>
45
+ </tbody>
46
+ </table>
 
 
 
 
 
 
 
47
  </div>
48
+ </section>
49
 
50
+ <section class="split-grid">
51
+ <article class="card" id="provider-detail">
52
+ <div class="section-heading">
53
+ <h2>Provider Detail</h2>
54
+ <span class="badge info" id="selected-provider">Select a provider</span>
 
 
 
 
55
  </div>
56
+ <ul class="list" id="provider-detail-list"></ul>
57
+ </article>
58
+ <article class="card">
59
+ <div class="section-heading">
60
+ <h2>Configuration Snapshot</h2>
61
+ <span class="badge info" id="config-summary">Loading...</span>
 
 
 
 
62
  </div>
63
+ <ul class="list" id="config-list"></ul>
64
+ </article>
65
+ </section>
66
+
67
+ <section class="split-grid">
68
+ <article class="card">
69
+ <div class="section-heading"><h2>Logs ( /api/logs )</h2><span class="badge info">Latest</span></div>
70
+ <div id="logs-list" class="ws-stream"></div>
71
+ </article>
72
+ <article class="card">
73
+ <div class="section-heading"><h2>Alerts ( /api/alerts )</h2><span class="badge info">Live</span></div>
74
+ <div id="alerts-list"></div>
75
+ </article>
76
+ </section>
77
+ </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </body>
79
+ </html>
ai_models.py CHANGED
@@ -8,6 +8,28 @@ from dataclasses import dataclass
8
  from typing import Any, Dict, List, Mapping, Optional, Sequence
9
  from config import HUGGINGFACE_MODELS, get_settings
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  try:
12
  from transformers import pipeline
13
  TRANSFORMERS_AVAILABLE = True
@@ -17,18 +39,38 @@ except ImportError:
17
  logger = logging.getLogger(__name__)
18
  settings = get_settings()
19
 
20
- # Extended Model Catalog
21
- CRYPTO_SENTIMENT_MODELS = [
22
- "ElKulako/cryptobert", "kk08/CryptoBERT",
23
- "burakutf/finetuned-finbert-crypto", "mathugo/crypto_news_bert"
 
 
 
 
 
 
 
 
 
 
 
24
  ]
25
- SOCIAL_SENTIMENT_MODELS = [
 
 
 
26
  "svalabs/twitter-xlm-roberta-bitcoin-sentiment",
27
- "mayurjadhav/crypto-sentiment-model"
 
 
 
28
  ]
29
- FINANCIAL_SENTIMENT_MODELS = ["ProsusAI/finbert", "cardiffnlp/twitter-roberta-base-sentiment"]
30
- NEWS_SENTIMENT_MODELS = ["mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis"]
31
- DECISION_MODELS = ["agarkovv/CryptoTrader-LM"]
 
 
 
32
 
33
  @dataclass(frozen=True)
34
  class PipelineSpec:
@@ -50,26 +92,29 @@ for lk in ["sentiment_twitter", "sentiment_financial", "summarization", "crypto_
50
  category="legacy"
51
  )
52
 
53
- # Crypto sentiment
 
 
 
 
 
 
54
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
55
  MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
56
  key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
57
  category="crypto_sentiment", requires_auth=("ElKulako" in mid)
58
  )
59
 
60
- # Social
61
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
62
  MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
63
  key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
64
  )
65
 
66
- # Financial
67
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
68
  MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
69
  key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
70
  )
71
 
72
- # News
73
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
74
  MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
75
  key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
@@ -97,44 +142,118 @@ class ModelRegistry:
97
  if key in self._pipelines:
98
  return self._pipelines[key]
99
 
100
- auth = settings.hf_token if spec.requires_auth else None
101
- logger.info(f"Loading model: {spec.model_id}")
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
- self._pipelines[key] = pipeline(spec.task, model=spec.model_id, tokenizer=spec.model_id, use_auth_token=auth)
 
 
 
 
 
 
 
 
 
104
  except Exception as e:
105
- logger.exception(f"Failed to load {spec.model_id}")
106
- raise ModelNotAvailable(str(e)) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  return self._pipelines[key]
 
 
 
 
 
 
 
 
109
 
110
  def initialize_models(self):
111
  if self._initialized:
112
- return {"status": "already_initialized", "models_loaded": len(self._pipelines)}
 
 
 
 
 
113
  if not TRANSFORMERS_AVAILABLE:
114
- return {"status": "transformers_not_available", "models_loaded": 0}
115
 
116
  loaded, failed = [], []
117
- for key in ["crypto_sent_0", "financial_sent_0"]:
 
 
118
  try:
119
  self.get_pipeline(key)
120
  loaded.append(key)
 
 
121
  except Exception as e:
122
- failed.append((key, str(e)))
 
123
 
124
  self._initialized = True
125
- return {"status": "initialized", "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
 
126
 
127
  _registry = ModelRegistry()
128
 
129
- def initialize_models(): return _registry.initialize_models()
 
 
 
 
 
 
130
 
131
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
132
- if not TRANSFORMERS_AVAILABLE:
133
- return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "transformers N/A"}
134
 
135
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
136
 
137
- for key in ["crypto_sent_0", "crypto_sent_1"]:
 
 
 
 
 
 
138
  try:
139
  pipe = _registry.get_pipeline(key)
140
  res = pipe(text[:512])
@@ -145,15 +264,20 @@ def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
145
 
146
  mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
147
 
148
- spec = MODEL_SPECS[key]
149
- results[spec.model_id] = {"label": mapped, "score": score}
 
 
 
150
  labels_count[mapped] += 1
151
  total_conf += score
 
 
152
  except Exception as e:
153
  logger.warning(f"Ensemble failed for {key}: {e}")
154
 
155
  if not results:
156
- return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0}
157
 
158
  final = max(labels_count, key=labels_count.get)
159
  avg_conf = total_conf / len(results)
@@ -211,16 +335,11 @@ def analyze_news_item(item: Dict[str, Any]):
211
  def get_model_info():
212
  return {
213
  "transformers_available": TRANSFORMERS_AVAILABLE,
214
- "hf_auth_configured": bool(settings.hf_token),
 
215
  "models_initialized": _registry._initialized,
216
  "models_loaded": len(_registry._pipelines),
217
- "model_catalog": {
218
- "crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
219
- "social_sentiment": SOCIAL_SENTIMENT_MODELS,
220
- "financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
221
- "news_sentiment": NEWS_SENTIMENT_MODELS,
222
- "decision": DECISION_MODELS
223
- },
224
  "total_models": len(MODEL_SPECS)
225
  }
226
 
 
8
  from typing import Any, Dict, List, Mapping, Optional, Sequence
9
  from config import HUGGINGFACE_MODELS, get_settings
10
 
11
+ # Set environment variables to avoid TensorFlow/Keras issues
12
+ # We'll force PyTorch framework instead
13
+ import os
14
+ import sys
15
+
16
+ # Completely disable TensorFlow to force PyTorch
17
+ os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
18
+ os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
19
+ os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3')
20
+ os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
21
+
22
+ # Mock tf_keras to prevent transformers from trying to import it
23
+ # This prevents the broken tf-keras installation from causing errors
24
+ class TfKerasMock:
25
+ """Mock tf_keras to prevent import errors when transformers checks for TensorFlow"""
26
+ pass
27
+
28
+ # Add mock to sys.modules before transformers imports
29
+ sys.modules['tf_keras'] = TfKerasMock()
30
+ sys.modules['tf_keras.src'] = TfKerasMock()
31
+ sys.modules['tf_keras.src.utils'] = TfKerasMock()
32
+
33
  try:
34
  from transformers import pipeline
35
  TRANSFORMERS_AVAILABLE = True
 
39
  logger = logging.getLogger(__name__)
40
  settings = get_settings()
41
 
42
+ HF_MODE = os.getenv("HF_MODE", "off").lower()
43
+ HF_TOKEN_ENV = os.getenv("HF_TOKEN")
44
+
45
+ if HF_MODE not in ("off", "public", "auth"):
46
+ HF_MODE = "off"
47
+ logger.warning(f"Invalid HF_MODE, defaulting to 'off'")
48
+
49
+ if HF_MODE == "auth" and not HF_TOKEN_ENV:
50
+ HF_MODE = "off"
51
+ logger.warning("HF_MODE='auth' but HF_TOKEN not set, defaulting to 'off'")
52
+
53
+ ACTIVE_MODELS = [
54
+ "ElKulako/cryptobert",
55
+ "kk08/CryptoBERT",
56
+ "ProsusAI/finbert"
57
  ]
58
+
59
+ LEGACY_MODELS = [
60
+ "burakutf/finetuned-finbert-crypto",
61
+ "mathugo/crypto_news_bert",
62
  "svalabs/twitter-xlm-roberta-bitcoin-sentiment",
63
+ "mayurjadhav/crypto-sentiment-model",
64
+ "cardiffnlp/twitter-roberta-base-sentiment",
65
+ "mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis",
66
+ "agarkovv/CryptoTrader-LM"
67
  ]
68
+
69
+ CRYPTO_SENTIMENT_MODELS = ACTIVE_MODELS[:2] + LEGACY_MODELS[:2]
70
+ SOCIAL_SENTIMENT_MODELS = LEGACY_MODELS[2:4]
71
+ FINANCIAL_SENTIMENT_MODELS = [ACTIVE_MODELS[2]] + [LEGACY_MODELS[4]]
72
+ NEWS_SENTIMENT_MODELS = [LEGACY_MODELS[5]]
73
+ DECISION_MODELS = [LEGACY_MODELS[6]]
74
 
75
  @dataclass(frozen=True)
76
  class PipelineSpec:
 
92
  category="legacy"
93
  )
94
 
95
+ for i, mid in enumerate(ACTIVE_MODELS):
96
+ MODEL_SPECS[f"active_{i}"] = PipelineSpec(
97
+ key=f"active_{i}", task="sentiment-analysis", model_id=mid,
98
+ category="crypto_sentiment" if i < 2 else "financial_sentiment",
99
+ requires_auth=("ElKulako" in mid)
100
+ )
101
+
102
  for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
103
  MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
104
  key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
105
  category="crypto_sentiment", requires_auth=("ElKulako" in mid)
106
  )
107
 
 
108
  for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
109
  MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
110
  key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
111
  )
112
 
 
113
  for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
114
  MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
115
  key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
116
  )
117
 
 
118
  for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
119
  MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
120
  key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
 
142
  if key in self._pipelines:
143
  return self._pipelines[key]
144
 
145
+ if HF_MODE == "off":
146
+ raise ModelNotAvailable("HF_MODE=off")
147
+
148
+ token_value = None
149
+ if HF_MODE == "auth":
150
+ token_value = HF_TOKEN_ENV or settings.hf_token
151
+ elif HF_MODE == "public":
152
+ token_value = None
153
+
154
+ if spec.requires_auth and not token_value:
155
+ raise ModelNotAvailable("Model requires auth but no token available")
156
+
157
+ logger.info(f"Loading model: {spec.model_id} (mode: {HF_MODE})")
158
  try:
159
+ pipeline_kwargs = {
160
+ 'task': spec.task,
161
+ 'model': spec.model_id,
162
+ 'tokenizer': spec.model_id,
163
+ 'framework': 'pt',
164
+ 'device': -1,
165
+ }
166
+ pipeline_kwargs['token'] = token_value
167
+
168
+ self._pipelines[key] = pipeline(**pipeline_kwargs)
169
  except Exception as e:
170
+ error_msg = str(e)
171
+ error_lower = error_msg.lower()
172
+
173
+ try:
174
+ from huggingface_hub.errors import RepositoryNotFoundError, HfHubHTTPError
175
+ hf_errors = (RepositoryNotFoundError, HfHubHTTPError)
176
+ except ImportError:
177
+ hf_errors = ()
178
+
179
+ is_auth_error = any(kw in error_lower for kw in ['401', 'unauthorized', 'repository not found', 'expired', 'token'])
180
+ is_hf_error = isinstance(e, hf_errors) or is_auth_error
181
+
182
+ if is_hf_error:
183
+ logger.warning(f"HF error for {spec.model_id}: {type(e).__name__}")
184
+ raise ModelNotAvailable(f"HF error: {spec.model_id}") from e
185
+
186
+ if any(kw in error_lower for kw in ['keras', 'tensorflow', 'tf_keras', 'framework']):
187
+ try:
188
+ pipeline_kwargs['torch_dtype'] = 'float32'
189
+ self._pipelines[key] = pipeline(**pipeline_kwargs)
190
+ return self._pipelines[key]
191
+ except Exception:
192
+ raise ModelNotAvailable(f"Framework error: {spec.model_id}") from e
193
+
194
+ raise ModelNotAvailable(f"Load failed: {spec.model_id}") from e
195
 
196
  return self._pipelines[key]
197
+
198
+ def get_loaded_models(self):
199
+ """Get list of all loaded model keys"""
200
+ return list(self._pipelines.keys())
201
+
202
+ def get_available_sentiment_models(self):
203
+ """Get list of all available sentiment model keys"""
204
+ return [key for key in MODEL_SPECS.keys() if "sent" in key or "sentiment" in key]
205
 
206
  def initialize_models(self):
207
  if self._initialized:
208
+ return {"status": "already_initialized", "mode": HF_MODE, "models_loaded": len(self._pipelines)}
209
+
210
+ if HF_MODE == "off":
211
+ self._initialized = True
212
+ return {"status": "disabled", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
213
+
214
  if not TRANSFORMERS_AVAILABLE:
215
+ return {"status": "transformers_not_available", "mode": HF_MODE, "models_loaded": 0}
216
 
217
  loaded, failed = [], []
218
+ active_keys = [f"active_{i}" for i in range(len(ACTIVE_MODELS))]
219
+
220
+ for key in active_keys:
221
  try:
222
  self.get_pipeline(key)
223
  loaded.append(key)
224
+ except ModelNotAvailable as e:
225
+ failed.append((key, str(e)[:100]))
226
  except Exception as e:
227
+ error_msg = str(e)[:100]
228
+ failed.append((key, error_msg))
229
 
230
  self._initialized = True
231
+ status = "initialized" if loaded else "partial"
232
+ return {"status": status, "mode": HF_MODE, "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
233
 
234
  _registry = ModelRegistry()
235
 
236
+ AI_MODELS_SUMMARY = {"status": "not_initialized", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
237
+
238
+ def initialize_models():
239
+ global AI_MODELS_SUMMARY
240
+ result = _registry.initialize_models()
241
+ AI_MODELS_SUMMARY = result
242
+ return result
243
 
244
  def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
245
+ if not TRANSFORMERS_AVAILABLE or HF_MODE == "off":
246
+ return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "HF disabled" if HF_MODE == "off" else "transformers N/A"}
247
 
248
  results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
249
 
250
+ loaded_keys = _registry.get_loaded_models()
251
+ available_keys = [key for key in loaded_keys if "sent" in key or "sentiment" in key or key.startswith("active_")]
252
+
253
+ if not available_keys:
254
+ return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "No models loaded"}
255
+
256
+ for key in available_keys:
257
  try:
258
  pipe = _registry.get_pipeline(key)
259
  res = pipe(text[:512])
 
264
 
265
  mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
266
 
267
+ spec = MODEL_SPECS.get(key)
268
+ if spec:
269
+ results[spec.model_id] = {"label": mapped, "score": score}
270
+ else:
271
+ results[key] = {"label": mapped, "score": score}
272
  labels_count[mapped] += 1
273
  total_conf += score
274
+ except ModelNotAvailable:
275
+ continue
276
  except Exception as e:
277
  logger.warning(f"Ensemble failed for {key}: {e}")
278
 
279
  if not results:
280
+ return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "All models failed"}
281
 
282
  final = max(labels_count, key=labels_count.get)
283
  avg_conf = total_conf / len(results)
 
335
  def get_model_info():
336
  return {
337
  "transformers_available": TRANSFORMERS_AVAILABLE,
338
+ "hf_mode": HF_MODE,
339
+ "hf_token_configured": bool(HF_TOKEN_ENV or settings.hf_token) if HF_MODE == "auth" else False,
340
  "models_initialized": _registry._initialized,
341
  "models_loaded": len(_registry._pipelines),
342
+ "active_models": ACTIVE_MODELS,
 
 
 
 
 
 
343
  "total_models": len(MODEL_SPECS)
344
  }
345
 
api-resources/crypto_resources_unified_2025-11-11.json CHANGED
The diff for this file is too large to render. See raw diff
 
app.js CHANGED
@@ -133,8 +133,41 @@ class WebSocketClient {
133
  }
134
 
135
  handleError(error) {
136
- console.error('[WS] Error:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  this.updateStatus('error');
 
 
 
 
 
 
138
  }
139
 
140
  handleClose() {
 
133
  }
134
 
135
  handleError(error) {
136
+ // WebSocket error events don't provide detailed error info
137
+ // Check socket state to provide better error context
138
+ const socketState = this.socket ? this.socket.readyState : 'null';
139
+ const stateNames = {
140
+ 0: 'CONNECTING',
141
+ 1: 'OPEN',
142
+ 2: 'CLOSING',
143
+ 3: 'CLOSED'
144
+ };
145
+
146
+ const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`;
147
+
148
+ // Only log error once to prevent spam
149
+ if (!this._errorLogged) {
150
+ console.error('[WS] Connection error:', {
151
+ url: this.url,
152
+ state: stateName,
153
+ readyState: socketState,
154
+ message: 'WebSocket connection failed. Check if server is running and URL is correct.'
155
+ });
156
+ this._errorLogged = true;
157
+
158
+ // Reset error flag after a delay to allow logging if error persists
159
+ setTimeout(() => {
160
+ this._errorLogged = false;
161
+ }, 5000);
162
+ }
163
+
164
  this.updateStatus('error');
165
+
166
+ // Attempt reconnection if not already scheduled
167
+ if (this.socket && this.socket.readyState === WebSocket.CLOSED &&
168
+ this.reconnectAttempts < this.maxReconnectAttempts) {
169
+ this.scheduleReconnect();
170
+ }
171
  }
172
 
173
  handleClose() {
app.py CHANGED
@@ -1194,6 +1194,37 @@ demo = build_interface()
1194
  if __name__ == "__main__":
1195
  logger.info("Launching Gradio dashboard...")
1196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1197
  demo.launch(
1198
  server_name="0.0.0.0",
1199
  server_port=7860,
 
1194
  if __name__ == "__main__":
1195
  logger.info("Launching Gradio dashboard...")
1196
 
1197
+ # Try to mount FastAPI app for API endpoints
1198
+ try:
1199
+ from fastapi import FastAPI as FastAPIApp
1200
+ from fastapi.middleware.wsgi import WSGIMiddleware
1201
+ import uvicorn
1202
+ from threading import Thread
1203
+ import time
1204
+
1205
+ # Import the FastAPI app from hf_unified_server
1206
+ try:
1207
+ from hf_unified_server import app as fastapi_app
1208
+ logger.info("✅ FastAPI app imported successfully")
1209
+
1210
+ # Start FastAPI server in a separate thread on port 7861
1211
+ def run_fastapi():
1212
+ uvicorn.run(
1213
+ fastapi_app,
1214
+ host="0.0.0.0",
1215
+ port=7861,
1216
+ log_level="info"
1217
+ )
1218
+
1219
+ fastapi_thread = Thread(target=run_fastapi, daemon=True)
1220
+ fastapi_thread.start()
1221
+ time.sleep(2) # Give FastAPI time to start
1222
+ logger.info("✅ FastAPI server started on port 7861")
1223
+ except ImportError as e:
1224
+ logger.warning(f"⚠️ Could not import FastAPI app: {e}")
1225
+ except Exception as e:
1226
+ logger.warning(f"⚠️ Could not start FastAPI server: {e}")
1227
+
1228
  demo.launch(
1229
  server_name="0.0.0.0",
1230
  server_port=7860,
archive_html/admin_advanced.html ADDED
@@ -0,0 +1,1862 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Advanced Admin Dashboard - Crypto Monitor</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
9
+ <style>
10
+ * { margin: 0; padding: 0; box-sizing: border-box; }
11
+
12
+ :root {
13
+ --primary: #6366f1;
14
+ --primary-dark: #4f46e5;
15
+ --primary-glow: rgba(99, 102, 241, 0.4);
16
+ --success: #10b981;
17
+ --warning: #f59e0b;
18
+ --danger: #ef4444;
19
+ --info: #3b82f6;
20
+ --bg-dark: #0f172a;
21
+ --bg-card: rgba(30, 41, 59, 0.7);
22
+ --bg-glass: rgba(30, 41, 59, 0.5);
23
+ --bg-hover: rgba(51, 65, 85, 0.8);
24
+ --text-light: #f1f5f9;
25
+ --text-muted: #94a3b8;
26
+ --border: rgba(51, 65, 85, 0.6);
27
+ }
28
+
29
+ body {
30
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
31
+ background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 50%, #000000 100%);
32
+ color: var(--text-light);
33
+ line-height: 1.6;
34
+ min-height: 100vh;
35
+ position: relative;
36
+ overflow-x: hidden;
37
+ }
38
+
39
+ /* Animated Background Particles */
40
+ body::before {
41
+ content: '';
42
+ position: fixed;
43
+ top: 0;
44
+ left: 0;
45
+ width: 100%;
46
+ height: 100%;
47
+ background:
48
+ radial-gradient(circle at 20% 50%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
49
+ radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
50
+ radial-gradient(circle at 40% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
51
+ animation: float 20s ease-in-out infinite;
52
+ pointer-events: none;
53
+ z-index: 0;
54
+ }
55
+
56
+ @keyframes float {
57
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
58
+ 33% { transform: translate(30px, -30px) rotate(120deg); }
59
+ 66% { transform: translate(-20px, 20px) rotate(240deg); }
60
+ }
61
+
62
+ .container {
63
+ max-width: 1800px;
64
+ margin: 0 auto;
65
+ padding: 20px;
66
+ position: relative;
67
+ z-index: 1;
68
+ }
69
+
70
+ /* Glassmorphic Header with Glow */
71
+ header {
72
+ background: linear-gradient(135deg, rgba(99, 102, 241, 0.9) 0%, rgba(79, 70, 229, 0.9) 100%);
73
+ backdrop-filter: blur(20px);
74
+ -webkit-backdrop-filter: blur(20px);
75
+ padding: 30px;
76
+ border-radius: 20px;
77
+ margin-bottom: 30px;
78
+ border: 1px solid rgba(255, 255, 255, 0.2);
79
+ box-shadow:
80
+ 0 8px 32px rgba(0, 0, 0, 0.3),
81
+ 0 0 60px var(--primary-glow),
82
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
83
+ position: relative;
84
+ overflow: hidden;
85
+ animation: headerGlow 3s ease-in-out infinite alternate;
86
+ }
87
+
88
+ @keyframes headerGlow {
89
+ 0% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 40px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
90
+ 100% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 80px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
91
+ }
92
+
93
+ header::before {
94
+ content: '';
95
+ position: absolute;
96
+ top: -50%;
97
+ left: -50%;
98
+ width: 200%;
99
+ height: 200%;
100
+ background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
101
+ transform: rotate(45deg);
102
+ animation: headerShine 3s linear infinite;
103
+ }
104
+
105
+ @keyframes headerShine {
106
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
107
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
108
+ }
109
+
110
+ header h1 {
111
+ font-size: 36px;
112
+ font-weight: 700;
113
+ margin-bottom: 8px;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 15px;
117
+ position: relative;
118
+ z-index: 1;
119
+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
120
+ }
121
+
122
+ header .icon {
123
+ font-size: 42px;
124
+ filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.5));
125
+ animation: iconPulse 2s ease-in-out infinite;
126
+ }
127
+
128
+ @keyframes iconPulse {
129
+ 0%, 100% { transform: scale(1); }
130
+ 50% { transform: scale(1.1); }
131
+ }
132
+
133
+ header .subtitle {
134
+ color: rgba(255, 255, 255, 0.95);
135
+ font-size: 16px;
136
+ position: relative;
137
+ z-index: 1;
138
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
139
+ }
140
+
141
+ /* Glassmorphic Tabs */
142
+ .tabs {
143
+ display: flex;
144
+ gap: 10px;
145
+ margin-bottom: 30px;
146
+ flex-wrap: wrap;
147
+ background: var(--bg-glass);
148
+ backdrop-filter: blur(10px);
149
+ -webkit-backdrop-filter: blur(10px);
150
+ padding: 15px;
151
+ border-radius: 16px;
152
+ border: 1px solid var(--border);
153
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
154
+ }
155
+
156
+ .tab-btn {
157
+ padding: 12px 24px;
158
+ background: rgba(255, 255, 255, 0.05);
159
+ backdrop-filter: blur(10px);
160
+ border: 1px solid rgba(255, 255, 255, 0.1);
161
+ border-radius: 10px;
162
+ cursor: pointer;
163
+ font-weight: 600;
164
+ color: var(--text-light);
165
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
166
+ position: relative;
167
+ overflow: hidden;
168
+ }
169
+
170
+ .tab-btn::before {
171
+ content: '';
172
+ position: absolute;
173
+ top: 0;
174
+ left: -100%;
175
+ width: 100%;
176
+ height: 100%;
177
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
178
+ transition: left 0.5s;
179
+ }
180
+
181
+ .tab-btn:hover::before {
182
+ left: 100%;
183
+ }
184
+
185
+ .tab-btn:hover {
186
+ background: rgba(99, 102, 241, 0.2);
187
+ border-color: var(--primary);
188
+ transform: translateY(-2px);
189
+ box-shadow: 0 4px 12px var(--primary-glow);
190
+ }
191
+
192
+ .tab-btn.active {
193
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
194
+ border-color: var(--primary);
195
+ box-shadow: 0 4px 20px var(--primary-glow);
196
+ transform: scale(1.05);
197
+ }
198
+
199
+ .tab-content {
200
+ display: none;
201
+ animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
202
+ }
203
+
204
+ .tab-content.active {
205
+ display: block;
206
+ }
207
+
208
+ @keyframes fadeInUp {
209
+ from {
210
+ opacity: 0;
211
+ transform: translateY(20px);
212
+ }
213
+ to {
214
+ opacity: 1;
215
+ transform: translateY(0);
216
+ }
217
+ }
218
+
219
+ /* Glassmorphic Cards */
220
+ .card {
221
+ background: var(--bg-glass);
222
+ backdrop-filter: blur(10px);
223
+ -webkit-backdrop-filter: blur(10px);
224
+ border-radius: 16px;
225
+ padding: 24px;
226
+ margin-bottom: 20px;
227
+ border: 1px solid var(--border);
228
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
229
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
230
+ }
231
+
232
+ .card:hover {
233
+ transform: translateY(-2px);
234
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
235
+ border-color: rgba(99, 102, 241, 0.3);
236
+ }
237
+
238
+ .card h3 {
239
+ color: var(--primary);
240
+ margin-bottom: 20px;
241
+ font-size: 20px;
242
+ display: flex;
243
+ align-items: center;
244
+ gap: 10px;
245
+ text-shadow: 0 0 20px var(--primary-glow);
246
+ }
247
+
248
+ /* Animated Stat Cards */
249
+ .stats-grid {
250
+ display: grid;
251
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
252
+ gap: 20px;
253
+ margin-bottom: 30px;
254
+ }
255
+
256
+ .stat-card {
257
+ background: var(--bg-glass);
258
+ backdrop-filter: blur(10px);
259
+ -webkit-backdrop-filter: blur(10px);
260
+ padding: 24px;
261
+ border-radius: 16px;
262
+ border: 1px solid var(--border);
263
+ position: relative;
264
+ overflow: hidden;
265
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
266
+ animation: statCardIn 0.5s ease-out backwards;
267
+ }
268
+
269
+ @keyframes statCardIn {
270
+ from {
271
+ opacity: 0;
272
+ transform: scale(0.9) translateY(20px);
273
+ }
274
+ to {
275
+ opacity: 1;
276
+ transform: scale(1) translateY(0);
277
+ }
278
+ }
279
+
280
+ .stat-card:nth-child(1) { animation-delay: 0.1s; }
281
+ .stat-card:nth-child(2) { animation-delay: 0.2s; }
282
+ .stat-card:nth-child(3) { animation-delay: 0.3s; }
283
+ .stat-card:nth-child(4) { animation-delay: 0.4s; }
284
+
285
+ .stat-card::before {
286
+ content: '';
287
+ position: absolute;
288
+ top: 0;
289
+ left: 0;
290
+ right: 0;
291
+ height: 3px;
292
+ background: linear-gradient(90deg, var(--primary), var(--info), var(--success));
293
+ background-size: 200% 100%;
294
+ animation: gradientMove 3s ease infinite;
295
+ }
296
+
297
+ @keyframes gradientMove {
298
+ 0%, 100% { background-position: 0% 50%; }
299
+ 50% { background-position: 100% 50%; }
300
+ }
301
+
302
+ .stat-card:hover {
303
+ transform: translateY(-8px) scale(1.02);
304
+ box-shadow: 0 12px 40px rgba(99, 102, 241, 0.3);
305
+ border-color: var(--primary);
306
+ }
307
+
308
+ .stat-card .label {
309
+ color: var(--text-muted);
310
+ font-size: 13px;
311
+ text-transform: uppercase;
312
+ letter-spacing: 0.5px;
313
+ font-weight: 600;
314
+ margin-bottom: 8px;
315
+ }
316
+
317
+ .stat-card .value {
318
+ font-size: 42px;
319
+ font-weight: 700;
320
+ margin: 8px 0;
321
+ color: var(--primary);
322
+ text-shadow: 0 0 30px var(--primary-glow);
323
+ animation: valueCount 1s ease-out;
324
+ }
325
+
326
+ @keyframes valueCount {
327
+ from { opacity: 0; transform: translateY(-10px); }
328
+ to { opacity: 1; transform: translateY(0); }
329
+ }
330
+
331
+ .stat-card .change {
332
+ font-size: 14px;
333
+ font-weight: 600;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 5px;
337
+ }
338
+
339
+ .stat-card .change.positive {
340
+ color: var(--success);
341
+ animation: bounce 1s ease-in-out infinite;
342
+ }
343
+
344
+ @keyframes bounce {
345
+ 0%, 100% { transform: translateY(0); }
346
+ 50% { transform: translateY(-3px); }
347
+ }
348
+
349
+ .stat-card .change.negative {
350
+ color: var(--danger);
351
+ }
352
+
353
+ /* Glassmorphic Chart Container */
354
+ .chart-container {
355
+ background: rgba(15, 23, 42, 0.5);
356
+ backdrop-filter: blur(10px);
357
+ padding: 20px;
358
+ border-radius: 12px;
359
+ margin-bottom: 20px;
360
+ height: 400px;
361
+ border: 1px solid rgba(255, 255, 255, 0.05);
362
+ box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2);
363
+ }
364
+
365
+ /* Modern Buttons */
366
+ .btn {
367
+ padding: 12px 24px;
368
+ border: none;
369
+ border-radius: 10px;
370
+ cursor: pointer;
371
+ font-weight: 600;
372
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
373
+ margin-right: 10px;
374
+ margin-bottom: 10px;
375
+ display: inline-flex;
376
+ align-items: center;
377
+ gap: 8px;
378
+ position: relative;
379
+ overflow: hidden;
380
+ backdrop-filter: blur(10px);
381
+ }
382
+
383
+ .btn::before {
384
+ content: '';
385
+ position: absolute;
386
+ top: 50%;
387
+ left: 50%;
388
+ width: 0;
389
+ height: 0;
390
+ border-radius: 50%;
391
+ background: rgba(255, 255, 255, 0.2);
392
+ transform: translate(-50%, -50%);
393
+ transition: width 0.6s, height 0.6s;
394
+ }
395
+
396
+ .btn:hover::before {
397
+ width: 300px;
398
+ height: 300px;
399
+ }
400
+
401
+ .btn-primary {
402
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
403
+ color: white;
404
+ box-shadow: 0 4px 15px var(--primary-glow);
405
+ }
406
+
407
+ .btn-primary:hover {
408
+ transform: translateY(-3px);
409
+ box-shadow: 0 8px 25px var(--primary-glow);
410
+ }
411
+
412
+ .btn-success {
413
+ background: linear-gradient(135deg, var(--success), #059669);
414
+ color: white;
415
+ box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
416
+ }
417
+
418
+ .btn-success:hover {
419
+ transform: translateY(-3px);
420
+ box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
421
+ }
422
+
423
+ .btn-warning {
424
+ background: linear-gradient(135deg, var(--warning), #d97706);
425
+ color: white;
426
+ box-shadow: 0 4px 15px rgba(245, 158, 11, 0.3);
427
+ }
428
+
429
+ .btn-danger {
430
+ background: linear-gradient(135deg, var(--danger), #dc2626);
431
+ color: white;
432
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
433
+ }
434
+
435
+ .btn-secondary {
436
+ background: rgba(51, 65, 85, 0.6);
437
+ color: var(--text-light);
438
+ border: 1px solid var(--border);
439
+ backdrop-filter: blur(10px);
440
+ }
441
+
442
+ .btn:disabled {
443
+ opacity: 0.5;
444
+ cursor: not-allowed;
445
+ transform: none !important;
446
+ }
447
+
448
+ .btn:active {
449
+ transform: scale(0.95);
450
+ }
451
+
452
+ /* Animated Progress Bar */
453
+ .progress-bar {
454
+ background: rgba(15, 23, 42, 0.8);
455
+ backdrop-filter: blur(10px);
456
+ height: 12px;
457
+ border-radius: 20px;
458
+ overflow: hidden;
459
+ margin-top: 10px;
460
+ border: 1px solid rgba(99, 102, 241, 0.3);
461
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
462
+ position: relative;
463
+ }
464
+
465
+ .progress-bar::before {
466
+ content: '';
467
+ position: absolute;
468
+ top: 0;
469
+ left: -100%;
470
+ width: 100%;
471
+ height: 100%;
472
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
473
+ animation: progressShine 2s linear infinite;
474
+ }
475
+
476
+ @keyframes progressShine {
477
+ 0% { left: -100%; }
478
+ 100% { left: 200%; }
479
+ }
480
+
481
+ .progress-bar-fill {
482
+ height: 100%;
483
+ background: linear-gradient(90deg, var(--primary), var(--info), var(--success));
484
+ background-size: 200% 100%;
485
+ animation: progressGradient 2s ease infinite;
486
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
487
+ box-shadow: 0 0 20px var(--primary-glow);
488
+ position: relative;
489
+ }
490
+
491
+ @keyframes progressGradient {
492
+ 0%, 100% { background-position: 0% 50%; }
493
+ 50% { background-position: 100% 50%; }
494
+ }
495
+
496
+ /* Glassmorphic Table */
497
+ table {
498
+ width: 100%;
499
+ border-collapse: collapse;
500
+ margin-top: 15px;
501
+ }
502
+
503
+ table thead {
504
+ background: rgba(15, 23, 42, 0.6);
505
+ backdrop-filter: blur(10px);
506
+ }
507
+
508
+ table th {
509
+ padding: 16px;
510
+ text-align: left;
511
+ font-weight: 600;
512
+ font-size: 12px;
513
+ text-transform: uppercase;
514
+ color: var(--text-muted);
515
+ border-bottom: 2px solid var(--border);
516
+ }
517
+
518
+ table td {
519
+ padding: 16px;
520
+ border-top: 1px solid var(--border);
521
+ transition: all 0.2s;
522
+ }
523
+
524
+ table tbody tr {
525
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
526
+ }
527
+
528
+ table tbody tr:hover {
529
+ background: var(--bg-hover);
530
+ backdrop-filter: blur(10px);
531
+ transform: scale(1.01);
532
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
533
+ }
534
+
535
+ /* Animated Resource Item */
536
+ .resource-item {
537
+ background: var(--bg-glass);
538
+ backdrop-filter: blur(10px);
539
+ padding: 16px;
540
+ border-radius: 12px;
541
+ margin-bottom: 12px;
542
+ border-left: 4px solid var(--primary);
543
+ display: flex;
544
+ justify-content: space-between;
545
+ align-items: center;
546
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
547
+ animation: slideIn 0.5s ease-out backwards;
548
+ }
549
+
550
+ @keyframes slideIn {
551
+ from {
552
+ opacity: 0;
553
+ transform: translateX(-20px);
554
+ }
555
+ to {
556
+ opacity: 1;
557
+ transform: translateX(0);
558
+ }
559
+ }
560
+
561
+ .resource-item:hover {
562
+ transform: translateX(5px) scale(1.02);
563
+ box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
564
+ }
565
+
566
+ .resource-item.duplicate {
567
+ border-left-color: var(--warning);
568
+ background: rgba(245, 158, 11, 0.1);
569
+ }
570
+
571
+ .resource-item.error {
572
+ border-left-color: var(--danger);
573
+ background: rgba(239, 68, 68, 0.1);
574
+ }
575
+
576
+ .resource-item.valid {
577
+ border-left-color: var(--success);
578
+ }
579
+
580
+ /* Animated Badges */
581
+ .badge {
582
+ display: inline-block;
583
+ padding: 6px 12px;
584
+ border-radius: 20px;
585
+ font-size: 11px;
586
+ font-weight: 600;
587
+ text-transform: uppercase;
588
+ backdrop-filter: blur(10px);
589
+ animation: badgePulse 2s ease-in-out infinite;
590
+ }
591
+
592
+ @keyframes badgePulse {
593
+ 0%, 100% { transform: scale(1); }
594
+ 50% { transform: scale(1.05); }
595
+ }
596
+
597
+ .badge-success {
598
+ background: rgba(16, 185, 129, 0.3);
599
+ color: var(--success);
600
+ box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);
601
+ }
602
+
603
+ .badge-warning {
604
+ background: rgba(245, 158, 11, 0.3);
605
+ color: var(--warning);
606
+ box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
607
+ }
608
+
609
+ .badge-danger {
610
+ background: rgba(239, 68, 68, 0.3);
611
+ color: var(--danger);
612
+ box-shadow: 0 0 15px rgba(239, 68, 68, 0.3);
613
+ }
614
+
615
+ .badge-info {
616
+ background: rgba(59, 130, 246, 0.3);
617
+ color: var(--info);
618
+ box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
619
+ }
620
+
621
+ /* Search/Filter Glassmorphic */
622
+ .search-bar {
623
+ display: flex;
624
+ gap: 15px;
625
+ margin-bottom: 20px;
626
+ flex-wrap: wrap;
627
+ }
628
+
629
+ .search-bar input,
630
+ .search-bar select {
631
+ padding: 12px;
632
+ border-radius: 10px;
633
+ border: 1px solid var(--border);
634
+ background: rgba(15, 23, 42, 0.6);
635
+ backdrop-filter: blur(10px);
636
+ color: var(--text-light);
637
+ flex: 1;
638
+ min-width: 200px;
639
+ transition: all 0.3s;
640
+ }
641
+
642
+ .search-bar input:focus,
643
+ .search-bar select:focus {
644
+ outline: none;
645
+ border-color: var(--primary);
646
+ box-shadow: 0 0 20px var(--primary-glow);
647
+ }
648
+
649
+ /* Loading Spinner with Glow */
650
+ .spinner {
651
+ border: 4px solid rgba(255, 255, 255, 0.1);
652
+ border-top-color: var(--primary);
653
+ border-radius: 50%;
654
+ width: 50px;
655
+ height: 50px;
656
+ animation: spin 0.8s linear infinite;
657
+ margin: 40px auto;
658
+ box-shadow: 0 0 30px var(--primary-glow);
659
+ }
660
+
661
+ @keyframes spin {
662
+ to { transform: rotate(360deg); }
663
+ }
664
+
665
+ /* Toast Notification with Glass */
666
+ .toast {
667
+ position: fixed;
668
+ bottom: 20px;
669
+ right: 20px;
670
+ background: var(--bg-glass);
671
+ backdrop-filter: blur(20px);
672
+ -webkit-backdrop-filter: blur(20px);
673
+ padding: 16px 24px;
674
+ border-radius: 12px;
675
+ border: 1px solid var(--border);
676
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
677
+ display: none;
678
+ align-items: center;
679
+ gap: 12px;
680
+ z-index: 1000;
681
+ animation: toastIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
682
+ }
683
+
684
+ @keyframes toastIn {
685
+ from {
686
+ transform: translateX(400px) scale(0.5);
687
+ opacity: 0;
688
+ }
689
+ to {
690
+ transform: translateX(0) scale(1);
691
+ opacity: 1;
692
+ }
693
+ }
694
+
695
+ .toast.show {
696
+ display: flex;
697
+ }
698
+
699
+ .toast.success {
700
+ border-left: 4px solid var(--success);
701
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(16, 185, 129, 0.3);
702
+ }
703
+
704
+ .toast.error {
705
+ border-left: 4px solid var(--danger);
706
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(239, 68, 68, 0.3);
707
+ }
708
+
709
+ /* Modal with Glass */
710
+ .modal {
711
+ display: none;
712
+ position: fixed;
713
+ top: 0;
714
+ left: 0;
715
+ right: 0;
716
+ bottom: 0;
717
+ background: rgba(0, 0, 0, 0.8);
718
+ backdrop-filter: blur(10px);
719
+ z-index: 1000;
720
+ align-items: center;
721
+ justify-content: center;
722
+ animation: fadeIn 0.3s;
723
+ }
724
+
725
+ .modal.show {
726
+ display: flex;
727
+ }
728
+
729
+ .modal-content {
730
+ background: var(--bg-glass);
731
+ backdrop-filter: blur(20px);
732
+ -webkit-backdrop-filter: blur(20px);
733
+ padding: 30px;
734
+ border-radius: 20px;
735
+ border: 1px solid var(--border);
736
+ max-width: 600px;
737
+ width: 90%;
738
+ max-height: 80vh;
739
+ overflow-y: auto;
740
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
741
+ animation: modalSlideIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
742
+ }
743
+
744
+ @keyframes modalSlideIn {
745
+ from {
746
+ transform: scale(0.5) translateY(-50px);
747
+ opacity: 0;
748
+ }
749
+ to {
750
+ transform: scale(1) translateY(0);
751
+ opacity: 1;
752
+ }
753
+ }
754
+
755
+ .modal-content h2 {
756
+ margin-bottom: 20px;
757
+ color: var(--primary);
758
+ text-shadow: 0 0 20px var(--primary-glow);
759
+ }
760
+
761
+ .modal-content .form-group {
762
+ margin-bottom: 20px;
763
+ }
764
+
765
+ .modal-content label {
766
+ display: block;
767
+ margin-bottom: 8px;
768
+ font-weight: 600;
769
+ color: var(--text-muted);
770
+ }
771
+
772
+ .modal-content input,
773
+ .modal-content textarea,
774
+ .modal-content select {
775
+ width: 100%;
776
+ padding: 12px;
777
+ border-radius: 10px;
778
+ border: 1px solid var(--border);
779
+ background: rgba(15, 23, 42, 0.6);
780
+ backdrop-filter: blur(10px);
781
+ color: var(--text-light);
782
+ transition: all 0.3s;
783
+ }
784
+
785
+ .modal-content input:focus,
786
+ .modal-content textarea:focus,
787
+ .modal-content select:focus {
788
+ outline: none;
789
+ border-color: var(--primary);
790
+ box-shadow: 0 0 20px var(--primary-glow);
791
+ }
792
+
793
+ .modal-content textarea {
794
+ min-height: 100px;
795
+ resize: vertical;
796
+ }
797
+
798
+ /* Grid Layout */
799
+ .grid-2 {
800
+ display: grid;
801
+ grid-template-columns: repeat(2, 1fr);
802
+ gap: 20px;
803
+ }
804
+
805
+ @media (max-width: 1024px) {
806
+ .grid-2 {
807
+ grid-template-columns: 1fr;
808
+ }
809
+ }
810
+
811
+ @media (max-width: 768px) {
812
+ .stats-grid {
813
+ grid-template-columns: 1fr;
814
+ }
815
+
816
+ header h1 {
817
+ font-size: 28px;
818
+ }
819
+
820
+ .tabs {
821
+ flex-direction: column;
822
+ }
823
+
824
+ .tab-btn {
825
+ width: 100%;
826
+ }
827
+ }
828
+
829
+ /* Scrollbar Styling */
830
+ ::-webkit-scrollbar {
831
+ width: 10px;
832
+ height: 10px;
833
+ }
834
+
835
+ ::-webkit-scrollbar-track {
836
+ background: rgba(15, 23, 42, 0.5);
837
+ border-radius: 10px;
838
+ }
839
+
840
+ ::-webkit-scrollbar-thumb {
841
+ background: linear-gradient(135deg, var(--primary), var(--info));
842
+ border-radius: 10px;
843
+ box-shadow: 0 0 10px var(--primary-glow);
844
+ }
845
+
846
+ ::-webkit-scrollbar-thumb:hover {
847
+ background: linear-gradient(135deg, var(--info), var(--success));
848
+ }
849
+ </style>
850
+ </head>
851
+ <body>
852
+ <div class="container">
853
+ <header>
854
+ <h1>
855
+ <span class="icon">📊</span>
856
+ Crypto Monitor Admin Dashboard
857
+ </h1>
858
+ <p class="subtitle">Real-time provider management & system monitoring | NO MOCK DATA</p>
859
+ </header>
860
+
861
+ <!-- Tabs -->
862
+ <div class="tabs">
863
+ <button class="tab-btn active" onclick="switchTab('dashboard')">📊 Dashboard</button>
864
+ <button class="tab-btn" onclick="switchTab('analytics')">📈 Analytics</button>
865
+ <button class="tab-btn" onclick="switchTab('resources')">🔧 Resource Manager</button>
866
+ <button class="tab-btn" onclick="switchTab('discovery')">🔍 Auto-Discovery</button>
867
+ <button class="tab-btn" onclick="switchTab('diagnostics')">🛠️ Diagnostics</button>
868
+ <button class="tab-btn" onclick="switchTab('logs')">📝 Logs</button>
869
+ </div>
870
+
871
+ <!-- Dashboard Tab -->
872
+ <div id="tab-dashboard" class="tab-content active">
873
+ <div class="stats-grid">
874
+ <div class="stat-card">
875
+ <div class="label">System Health</div>
876
+ <div class="value" id="system-health">HEALTHY</div>
877
+ <div class="change positive">✅ Healthy</div>
878
+ </div>
879
+
880
+ <div class="stat-card">
881
+ <div class="label">Total Providers</div>
882
+ <div class="value" id="total-providers">95</div>
883
+ <div class="change positive">↑ +12 this week</div>
884
+ </div>
885
+
886
+ <div class="stat-card">
887
+ <div class="label">Validated</div>
888
+ <div class="value" style="color: var(--success);" id="validated-count">32</div>
889
+ <div class="change positive">✓ All Active</div>
890
+ </div>
891
+
892
+ <div class="stat-card">
893
+ <div class="label">Database</div>
894
+ <div class="value">✓</div>
895
+ <div class="change positive">🗄️ Connected</div>
896
+ </div>
897
+ </div>
898
+
899
+ <div class="card">
900
+ <h3>⚡ Quick Actions</h3>
901
+ <button class="btn btn-primary" onclick="refreshAllData()">🔄 Refresh All</button>
902
+ <button class="btn btn-success" onclick="runAPLScan()">🤖 Run APL Scan</button>
903
+ <button class="btn btn-secondary" onclick="runDiagnostics(false)">🔧 Run Diagnostics</button>
904
+ </div>
905
+
906
+ <div class="card">
907
+ <h3>📊 Recent Market Data</h3>
908
+ <div class="progress-bar" style="margin-bottom: 20px;">
909
+ <div class="progress-bar-fill" style="width: 85%;"></div>
910
+ </div>
911
+ <div id="quick-market-view">Loading market data...</div>
912
+ </div>
913
+
914
+ <div class="grid-2">
915
+ <div class="card">
916
+ <h3>📈 Request Timeline (24h)</h3>
917
+ <div class="chart-container">
918
+ <canvas id="requestsChart"></canvas>
919
+ </div>
920
+ </div>
921
+
922
+ <div class="card">
923
+ <h3>🎯 Success vs Errors</h3>
924
+ <div class="chart-container">
925
+ <canvas id="statusChart"></canvas>
926
+ </div>
927
+ </div>
928
+ </div>
929
+ </div>
930
+
931
+ <!-- Analytics Tab -->
932
+ <div id="tab-analytics" class="tab-content">
933
+ <div class="card">
934
+ <h3>📈 Performance Analytics</h3>
935
+ <div class="search-bar">
936
+ <select id="analytics-timeframe">
937
+ <option value="1h">Last Hour</option>
938
+ <option value="24h" selected>Last 24 Hours</option>
939
+ <option value="7d">Last 7 Days</option>
940
+ <option value="30d">Last 30 Days</option>
941
+ </select>
942
+ <button class="btn btn-primary" onclick="refreshAnalytics()">🔄 Refresh</button>
943
+ <button class="btn btn-secondary" onclick="exportAnalytics()">📥 Export Data</button>
944
+ </div>
945
+
946
+ <div class="chart-container" style="height: 500px;">
947
+ <canvas id="performanceChart"></canvas>
948
+ </div>
949
+ </div>
950
+
951
+ <div class="grid-2">
952
+ <div class="card">
953
+ <h3>🏆 Top Performing Resources</h3>
954
+ <div id="top-resources">Loading...</div>
955
+ </div>
956
+
957
+ <div class="card">
958
+ <h3>⚠️ Resources with Issues</h3>
959
+ <div id="problem-resources">Loading...</div>
960
+ </div>
961
+ </div>
962
+ </div>
963
+
964
+ <!-- Resource Manager Tab -->
965
+ <div id="tab-resources" class="tab-content">
966
+ <div class="card">
967
+ <h3>🔧 Resource Management</h3>
968
+
969
+ <div class="search-bar">
970
+ <input type="text" id="resource-search" placeholder="🔍 Search resources..." oninput="filterResources()">
971
+ <select id="resource-filter" onchange="filterResources()">
972
+ <option value="all">All Resources</option>
973
+ <option value="valid">✅ Valid</option>
974
+ <option value="duplicate">⚠️ Duplicates</option>
975
+ <option value="error">❌ Errors</option>
976
+ <option value="hf-model">🤖 HF Models</option>
977
+ </select>
978
+ <button class="btn btn-primary" onclick="scanResources()">🔄 Scan All</button>
979
+ <button class="btn btn-success" onclick="openAddResourceModal()">➕ Add Resource</button>
980
+ </div>
981
+
982
+ <div class="card" style="background: rgba(245, 158, 11, 0.1); padding: 15px; margin-bottom: 20px;">
983
+ <div style="display: flex; justify-content: space-between; align-items: center;">
984
+ <div>
985
+ <strong>Duplicate Detection:</strong>
986
+ <span id="duplicate-count" class="badge badge-warning">0 found</span>
987
+ </div>
988
+ <button class="btn btn-warning" onclick="fixDuplicates()">🔧 Auto-Fix Duplicates</button>
989
+ </div>
990
+ </div>
991
+
992
+ <div id="resources-list">Loading resources...</div>
993
+ </div>
994
+
995
+ <div class="card">
996
+ <h3>🔄 Bulk Operations</h3>
997
+ <div style="display: flex; gap: 10px; flex-wrap: wrap;">
998
+ <button class="btn btn-success" onclick="validateAllResources()">✅ Validate All</button>
999
+ <button class="btn btn-warning" onclick="refreshAllResources()">🔄 Refresh All</button>
1000
+ <button class="btn btn-danger" onclick="removeInvalidResources()">🗑️ Remove Invalid</button>
1001
+ <button class="btn btn-secondary" onclick="exportResources()">📥 Export Config</button>
1002
+ <button class="btn btn-secondary" onclick="importResources()">📤 Import Config</button>
1003
+ </div>
1004
+ </div>
1005
+ </div>
1006
+
1007
+ <!-- Auto-Discovery Tab -->
1008
+ <div id="tab-discovery" class="tab-content">
1009
+ <div class="card">
1010
+ <h3>🔍 Auto-Discovery Engine</h3>
1011
+ <p style="color: var(--text-muted); margin-bottom: 20px;">
1012
+ Automatically discover, validate, and integrate new API providers and HuggingFace models.
1013
+ </p>
1014
+
1015
+ <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
1016
+ <button class="btn btn-success" onclick="runFullDiscovery()" id="discovery-btn">
1017
+ 🚀 Run Full Discovery
1018
+ </button>
1019
+ <button class="btn btn-primary" onclick="runAPLScan()">
1020
+ 🤖 APL Scan
1021
+ </button>
1022
+ <button class="btn btn-secondary" onclick="discoverHFModels()">
1023
+ 🧠 Discover HF Models
1024
+ </button>
1025
+ <button class="btn btn-secondary" onclick="discoverAPIs()">
1026
+ 🌐 Discover APIs
1027
+ </button>
1028
+ </div>
1029
+
1030
+ <div id="discovery-progress" style="display: none;">
1031
+ <div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
1032
+ <span>Discovery in progress...</span>
1033
+ <span id="discovery-percent">0%</span>
1034
+ </div>
1035
+ <div class="progress-bar">
1036
+ <div class="progress-bar-fill" id="discovery-progress-bar" style="width: 0%"></div>
1037
+ </div>
1038
+ </div>
1039
+
1040
+ <div id="discovery-results"></div>
1041
+ </div>
1042
+
1043
+ <div class="card">
1044
+ <h3>📊 Discovery Statistics</h3>
1045
+ <div class="stats-grid">
1046
+ <div class="stat-card">
1047
+ <div class="label">New Resources Found</div>
1048
+ <div class="value" id="discovery-found">0</div>
1049
+ </div>
1050
+ <div class="stat-card">
1051
+ <div class="label">Successfully Validated</div>
1052
+ <div class="value" id="discovery-validated" style="color: var(--success);">0</div>
1053
+ </div>
1054
+ <div class="stat-card">
1055
+ <div class="label">Failed Validation</div>
1056
+ <div class="value" id="discovery-failed" style="color: var(--danger);">0</div>
1057
+ </div>
1058
+ <div class="stat-card">
1059
+ <div class="label">Last Scan</div>
1060
+ <div class="value" id="discovery-last" style="font-size: 20px;">Never</div>
1061
+ </div>
1062
+ </div>
1063
+ </div>
1064
+ </div>
1065
+
1066
+ <!-- Diagnostics Tab -->
1067
+ <div id="tab-diagnostics" class="tab-content">
1068
+ <div class="card">
1069
+ <h3>🛠️ System Diagnostics</h3>
1070
+ <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
1071
+ <button class="btn btn-primary" onclick="runDiagnostics(false)">🔍 Scan Only</button>
1072
+ <button class="btn btn-success" onclick="runDiagnostics(true)">🔧 Scan & Auto-Fix</button>
1073
+ <button class="btn btn-secondary" onclick="testConnections()">🌐 Test Connections</button>
1074
+ <button class="btn btn-secondary" onclick="clearCache()">🗑️ Clear Cache</button>
1075
+ </div>
1076
+
1077
+ <div id="diagnostics-output">
1078
+ <p style="color: var(--text-muted);">Click a button above to run diagnostics...</p>
1079
+ </div>
1080
+ </div>
1081
+ </div>
1082
+
1083
+ <!-- Logs Tab -->
1084
+ <div id="tab-logs" class="tab-content">
1085
+ <div class="card">
1086
+ <h3>📝 System Logs</h3>
1087
+ <div class="search-bar">
1088
+ <select id="log-level" onchange="filterLogs()">
1089
+ <option value="all">All Levels</option>
1090
+ <option value="error">Errors Only</option>
1091
+ <option value="warning">Warnings</option>
1092
+ <option value="info">Info</option>
1093
+ </select>
1094
+ <input type="text" id="log-search" placeholder="Search logs..." oninput="filterLogs()">
1095
+ <button class="btn btn-primary" onclick="refreshLogs()">🔄 Refresh</button>
1096
+ <button class="btn btn-secondary" onclick="exportLogs()">📥 Export</button>
1097
+ <button class="btn btn-danger" onclick="clearLogs()">🗑️ Clear</button>
1098
+ </div>
1099
+
1100
+ <div id="logs-container" style="max-height: 600px; overflow-y: auto; background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; font-family: 'Courier New', monospace; font-size: 13px;">
1101
+ <p style="color: var(--text-muted);">Loading logs...</p>
1102
+ </div>
1103
+ </div>
1104
+ </div>
1105
+ </div>
1106
+
1107
+ <!-- Toast Notification -->
1108
+ <div class="toast" id="toast">
1109
+ <span id="toast-message"></span>
1110
+ </div>
1111
+
1112
+ <!-- Add Resource Modal -->
1113
+ <div class="modal" id="add-resource-modal" onclick="if(event.target === this) closeAddResourceModal()">
1114
+ <div class="modal-content">
1115
+ <h2>➕ Add New Resource</h2>
1116
+
1117
+ <div class="form-group">
1118
+ <label>Resource Type</label>
1119
+ <select id="new-resource-type">
1120
+ <option value="api">HTTP API</option>
1121
+ <option value="hf-model">HuggingFace Model</option>
1122
+ <option value="hf-dataset">HuggingFace Dataset</option>
1123
+ </select>
1124
+ </div>
1125
+
1126
+ <div class="form-group">
1127
+ <label>Name</label>
1128
+ <input type="text" id="new-resource-name" placeholder="Resource Name">
1129
+ </div>
1130
+
1131
+ <div class="form-group">
1132
+ <label>ID / URL</label>
1133
+ <input type="text" id="new-resource-url" placeholder="https://api.example.com or user/model">
1134
+ </div>
1135
+
1136
+ <div class="form-group">
1137
+ <label>Category</label>
1138
+ <input type="text" id="new-resource-category" placeholder="market_data, sentiment, etc.">
1139
+ </div>
1140
+
1141
+ <div class="form-group">
1142
+ <label>Notes (Optional)</label>
1143
+ <textarea id="new-resource-notes" placeholder="Additional information..."></textarea>
1144
+ </div>
1145
+
1146
+ <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
1147
+ <button class="btn btn-secondary" onclick="closeAddResourceModal()">Cancel</button>
1148
+ <button class="btn btn-success" onclick="addResource()">Add Resource</button>
1149
+ </div>
1150
+ </div>
1151
+ </div>
1152
+
1153
+ <script>
1154
+ // Global state
1155
+ let allResources = [];
1156
+ let apiStats = {
1157
+ totalRequests: 0,
1158
+ successRate: 0,
1159
+ avgResponseTime: 0,
1160
+ requestsHistory: []
1161
+ };
1162
+ let charts = {};
1163
+
1164
+ // Initialize
1165
+ document.addEventListener('DOMContentLoaded', function() {
1166
+ console.log('✨ Advanced Admin Dashboard Loaded');
1167
+ initCharts();
1168
+ loadDashboardData();
1169
+ startAutoRefresh();
1170
+ });
1171
+
1172
+ // Tab Switching
1173
+ function switchTab(tabName) {
1174
+ document.querySelectorAll('.tab-content').forEach(tab => {
1175
+ tab.classList.remove('active');
1176
+ });
1177
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1178
+ btn.classList.remove('active');
1179
+ });
1180
+
1181
+ document.getElementById(`tab-${tabName}`).classList.add('active');
1182
+ event.target.classList.add('active');
1183
+
1184
+ // Load tab-specific data
1185
+ switch(tabName) {
1186
+ case 'dashboard':
1187
+ loadDashboardData();
1188
+ break;
1189
+ case 'analytics':
1190
+ loadAnalytics();
1191
+ break;
1192
+ case 'resources':
1193
+ loadResources();
1194
+ break;
1195
+ case 'discovery':
1196
+ loadDiscoveryStats();
1197
+ break;
1198
+ case 'diagnostics':
1199
+ break;
1200
+ case 'logs':
1201
+ loadLogs();
1202
+ break;
1203
+ }
1204
+ }
1205
+
1206
+ // Initialize Charts with animations
1207
+ function initCharts() {
1208
+ Chart.defaults.color = '#94a3b8';
1209
+ Chart.defaults.borderColor = 'rgba(51, 65, 85, 0.3)';
1210
+
1211
+ // Requests Timeline Chart
1212
+ const requestsCtx = document.getElementById('requestsChart').getContext('2d');
1213
+ charts.requests = new Chart(requestsCtx, {
1214
+ type: 'line',
1215
+ data: {
1216
+ labels: [],
1217
+ datasets: [{
1218
+ label: 'API Requests',
1219
+ data: [],
1220
+ borderColor: '#6366f1',
1221
+ backgroundColor: 'rgba(99, 102, 241, 0.2)',
1222
+ tension: 0.4,
1223
+ fill: true,
1224
+ pointRadius: 4,
1225
+ pointHoverRadius: 6,
1226
+ borderWidth: 3
1227
+ }]
1228
+ },
1229
+ options: {
1230
+ responsive: true,
1231
+ maintainAspectRatio: false,
1232
+ animation: {
1233
+ duration: 1500,
1234
+ easing: 'easeInOutQuart'
1235
+ },
1236
+ plugins: {
1237
+ legend: { display: false }
1238
+ },
1239
+ scales: {
1240
+ y: {
1241
+ beginAtZero: true,
1242
+ ticks: { color: '#94a3b8' },
1243
+ grid: { color: 'rgba(51, 65, 85, 0.3)' }
1244
+ },
1245
+ x: {
1246
+ ticks: { color: '#94a3b8' },
1247
+ grid: { color: 'rgba(51, 65, 85, 0.3)' }
1248
+ }
1249
+ }
1250
+ }
1251
+ });
1252
+
1253
+ // Status Chart (Doughnut)
1254
+ const statusCtx = document.getElementById('statusChart').getContext('2d');
1255
+ charts.status = new Chart(statusCtx, {
1256
+ type: 'doughnut',
1257
+ data: {
1258
+ labels: ['Success', 'Errors', 'Timeouts'],
1259
+ datasets: [{
1260
+ data: [85, 10, 5],
1261
+ backgroundColor: [
1262
+ 'rgba(16, 185, 129, 0.8)',
1263
+ 'rgba(239, 68, 68, 0.8)',
1264
+ 'rgba(245, 158, 11, 0.8)'
1265
+ ],
1266
+ borderWidth: 3,
1267
+ borderColor: 'rgba(15, 23, 42, 0.5)'
1268
+ }]
1269
+ },
1270
+ options: {
1271
+ responsive: true,
1272
+ maintainAspectRatio: false,
1273
+ animation: {
1274
+ animateRotate: true,
1275
+ animateScale: true,
1276
+ duration: 2000,
1277
+ easing: 'easeOutBounce'
1278
+ },
1279
+ plugins: {
1280
+ legend: {
1281
+ position: 'bottom',
1282
+ labels: {
1283
+ color: '#94a3b8',
1284
+ padding: 15,
1285
+ font: { size: 13 }
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ });
1291
+
1292
+ // Performance Chart
1293
+ const perfCtx = document.getElementById('performanceChart').getContext('2d');
1294
+ charts.performance = new Chart(perfCtx, {
1295
+ type: 'bar',
1296
+ data: {
1297
+ labels: [],
1298
+ datasets: [{
1299
+ label: 'Response Time (ms)',
1300
+ data: [],
1301
+ backgroundColor: 'rgba(99, 102, 241, 0.7)',
1302
+ borderColor: '#6366f1',
1303
+ borderWidth: 2,
1304
+ borderRadius: 8
1305
+ }]
1306
+ },
1307
+ options: {
1308
+ responsive: true,
1309
+ maintainAspectRatio: false,
1310
+ animation: {
1311
+ duration: 1500,
1312
+ easing: 'easeOutQuart'
1313
+ },
1314
+ plugins: {
1315
+ legend: { display: false }
1316
+ },
1317
+ scales: {
1318
+ y: {
1319
+ beginAtZero: true,
1320
+ ticks: { color: '#94a3b8' },
1321
+ grid: { color: 'rgba(51, 65, 85, 0.3)' }
1322
+ },
1323
+ x: {
1324
+ ticks: { color: '#94a3b8' },
1325
+ grid: { color: 'rgba(51, 65, 85, 0.3)' }
1326
+ }
1327
+ }
1328
+ }
1329
+ });
1330
+ }
1331
+
1332
+ // Load Dashboard Data
1333
+ async function loadDashboardData() {
1334
+ try {
1335
+ const stats = await fetchAPIStats();
1336
+ updateDashboardStats(stats);
1337
+ updateCharts(stats);
1338
+ loadMarketPreview();
1339
+ } catch (error) {
1340
+ console.error('Error loading dashboard:', error);
1341
+ showToast('Failed to load dashboard data', 'error');
1342
+ }
1343
+ }
1344
+
1345
+ // Fetch API Statistics
1346
+ async function fetchAPIStats() {
1347
+ const stats = {
1348
+ totalRequests: 0,
1349
+ successRate: 0,
1350
+ avgResponseTime: 0,
1351
+ requestsHistory: [],
1352
+ statusBreakdown: { success: 0, errors: 0, timeouts: 0 }
1353
+ };
1354
+
1355
+ try {
1356
+ const providersResp = await fetch('/api/providers');
1357
+ if (providersResp.ok) {
1358
+ const providersData = await providersResp.json();
1359
+ const providers = providersData.providers || [];
1360
+
1361
+ stats.totalRequests = providers.length * 100;
1362
+ const validProviders = providers.filter(p => p.status === 'validated').length;
1363
+ stats.successRate = providers.length > 0 ? (validProviders / providers.length * 100).toFixed(1) : 0;
1364
+
1365
+ const responseTimes = providers
1366
+ .filter(p => p.response_time_ms)
1367
+ .map(p => p.response_time_ms);
1368
+ stats.avgResponseTime = responseTimes.length > 0
1369
+ ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
1370
+ : 0;
1371
+
1372
+ stats.statusBreakdown.success = validProviders;
1373
+ stats.statusBreakdown.errors = providers.length - validProviders;
1374
+ }
1375
+
1376
+ // Generate 24h timeline
1377
+ const now = Date.now();
1378
+ for (let i = 23; i >= 0; i--) {
1379
+ const time = new Date(now - i * 3600000);
1380
+ stats.requestsHistory.push({
1381
+ timestamp: time.toISOString(),
1382
+ count: Math.floor(Math.random() * 50) + 20
1383
+ });
1384
+ }
1385
+ } catch (error) {
1386
+ console.error('Error calculating stats:', error);
1387
+ }
1388
+
1389
+ return stats;
1390
+ }
1391
+
1392
+ // Update Dashboard Stats
1393
+ function updateDashboardStats(stats) {
1394
+ document.getElementById('total-providers').textContent = Math.floor(stats.totalRequests / 100);
1395
+ }
1396
+
1397
+ // Update Charts
1398
+ function updateCharts(stats) {
1399
+ if (stats.requestsHistory && charts.requests) {
1400
+ charts.requests.data.labels = stats.requestsHistory.map(r =>
1401
+ new Date(r.timestamp).toLocaleTimeString('en-US', { hour: '2-digit' })
1402
+ );
1403
+ charts.requests.data.datasets[0].data = stats.requestsHistory.map(r => r.count);
1404
+ charts.requests.update('active');
1405
+ }
1406
+
1407
+ if (stats.statusBreakdown && charts.status) {
1408
+ charts.status.data.datasets[0].data = [
1409
+ stats.statusBreakdown.success,
1410
+ stats.statusBreakdown.errors,
1411
+ stats.statusBreakdown.timeouts || 5
1412
+ ];
1413
+ charts.status.update('active');
1414
+ }
1415
+ }
1416
+
1417
+ // Load Market Preview
1418
+ async function loadMarketPreview() {
1419
+ try {
1420
+ const response = await fetch('/api/market');
1421
+ if (response.ok) {
1422
+ const data = await response.json();
1423
+ const coins = (data.cryptocurrencies || []).slice(0, 4);
1424
+
1425
+ const html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">' +
1426
+ coins.map(coin => `
1427
+ <div style="background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; border: 1px solid var(--border);">
1428
+ <div style="font-weight: 600;">${coin.name} (${coin.symbol})</div>
1429
+ <div style="font-size: 24px; margin: 10px 0; color: var(--primary);">$${coin.price.toLocaleString()}</div>
1430
+ <div style="color: ${coin.change_24h >= 0 ? 'var(--success)' : 'var(--danger)'};">
1431
+ ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%
1432
+ </div>
1433
+ </div>
1434
+ `).join('') +
1435
+ '</div>';
1436
+
1437
+ document.getElementById('quick-market-view').innerHTML = html;
1438
+ }
1439
+ } catch (error) {
1440
+ console.error('Error loading market preview:', error);
1441
+ document.getElementById('quick-market-view').innerHTML = '<p style="color: var(--text-muted);">Market data unavailable</p>';
1442
+ }
1443
+ }
1444
+
1445
+ // Load Resources
1446
+ async function loadResources() {
1447
+ try {
1448
+ const response = await fetch('/api/providers');
1449
+ const data = await response.json();
1450
+ allResources = data.providers || [];
1451
+
1452
+ detectDuplicates();
1453
+ renderResources(allResources);
1454
+ } catch (error) {
1455
+ console.error('Error loading resources:', error);
1456
+ showToast('Failed to load resources', 'error');
1457
+ }
1458
+ }
1459
+
1460
+ // Detect Duplicates
1461
+ function detectDuplicates() {
1462
+ const seen = new Set();
1463
+ const duplicates = [];
1464
+
1465
+ allResources.forEach(resource => {
1466
+ const key = resource.name.toLowerCase().replace(/[^a-z0-9]/g, '');
1467
+ if (seen.has(key)) {
1468
+ duplicates.push(resource.provider_id);
1469
+ resource.isDuplicate = true;
1470
+ } else {
1471
+ seen.add(key);
1472
+ resource.isDuplicate = false;
1473
+ }
1474
+ });
1475
+
1476
+ document.getElementById('duplicate-count').textContent = `${duplicates.length} found`;
1477
+ return duplicates;
1478
+ }
1479
+
1480
+ // Render Resources
1481
+ function renderResources(resources) {
1482
+ const container = document.getElementById('resources-list');
1483
+
1484
+ if (resources.length === 0) {
1485
+ container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);">No resources found</div>';
1486
+ return;
1487
+ }
1488
+
1489
+ container.innerHTML = resources.map((r, index) => `
1490
+ <div class="resource-item ${r.isDuplicate ? 'duplicate' : r.status === 'validated' ? 'valid' : 'error'}" style="animation-delay: ${index * 0.05}s;">
1491
+ <div class="resource-info" style="flex: 1;">
1492
+ <div class="name">
1493
+ ${r.name}
1494
+ ${r.isDuplicate ? '<span class="badge badge-warning">DUPLICATE</span>' : ''}
1495
+ ${r.status === 'validated' ? '<span class="badge badge-success">VALID</span>' : '<span class="badge badge-danger">INVALID</span>'}
1496
+ </div>
1497
+ <div class="details" style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">
1498
+ ID: <code style="color: var(--primary);">${r.provider_id}</code> |
1499
+ Category: ${r.category || 'N/A'} |
1500
+ Type: ${r.type || 'N/A'}
1501
+ ${r.response_time_ms ? ` | Response: ${Math.round(r.response_time_ms)}ms` : ''}
1502
+ </div>
1503
+ </div>
1504
+ <div class="resource-actions" style="display: flex; gap: 8px;">
1505
+ <button class="btn btn-primary" onclick="testResource('${r.provider_id}')">🧪 Test</button>
1506
+ <button class="btn btn-warning" onclick="editResource('${r.provider_id}')">✏️ Edit</button>
1507
+ <button class="btn btn-danger" onclick="removeResource('${r.provider_id}')">🗑️</button>
1508
+ </div>
1509
+ </div>
1510
+ `).join('');
1511
+ }
1512
+
1513
+ // Filter Resources
1514
+ function filterResources() {
1515
+ const search = document.getElementById('resource-search').value.toLowerCase();
1516
+ const filter = document.getElementById('resource-filter').value;
1517
+
1518
+ let filtered = allResources;
1519
+
1520
+ if (filter !== 'all') {
1521
+ filtered = filtered.filter(r => {
1522
+ if (filter === 'duplicate') return r.isDuplicate;
1523
+ if (filter === 'valid') return r.status === 'validated';
1524
+ if (filter === 'error') return r.status !== 'validated';
1525
+ if (filter === 'hf-model') return r.category === 'hf-model';
1526
+ return true;
1527
+ });
1528
+ }
1529
+
1530
+ if (search) {
1531
+ filtered = filtered.filter(r =>
1532
+ r.name.toLowerCase().includes(search) ||
1533
+ r.provider_id.toLowerCase().includes(search) ||
1534
+ (r.category && r.category.toLowerCase().includes(search))
1535
+ );
1536
+ }
1537
+
1538
+ renderResources(filtered);
1539
+ }
1540
+
1541
+ // Load Analytics
1542
+ async function loadAnalytics() {
1543
+ try {
1544
+ const response = await fetch('/api/providers');
1545
+ if (response.ok) {
1546
+ const data = await response.json();
1547
+ const providers = (data.providers || []).slice(0, 10);
1548
+
1549
+ charts.performance.data.labels = providers.map(p => p.name.substring(0, 20));
1550
+ charts.performance.data.datasets[0].data = providers.map(p => p.response_time_ms || 0);
1551
+ charts.performance.update('active');
1552
+
1553
+ // Top performers
1554
+ const topProviders = providers
1555
+ .filter(p => p.status === 'validated' && p.response_time_ms)
1556
+ .sort((a, b) => a.response_time_ms - b.response_time_ms)
1557
+ .slice(0, 5);
1558
+
1559
+ document.getElementById('top-resources').innerHTML = topProviders.map((p, i) => `
1560
+ <div style="padding: 12px; background: rgba(16, 185, 129, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--success);">
1561
+ <div style="display: flex; justify-content: space-between;">
1562
+ <div>
1563
+ <strong>${i + 1}. ${p.name}</strong>
1564
+ <div style="font-size: 12px; color: var(--text-muted);">${p.provider_id}</div>
1565
+ </div>
1566
+ <div style="text-align: right;">
1567
+ <div style="color: var(--success); font-weight: 600;">${Math.round(p.response_time_ms)}ms</div>
1568
+ <div style="font-size: 12px; color: var(--text-muted);">avg response</div>
1569
+ </div>
1570
+ </div>
1571
+ </div>
1572
+ `).join('') || '<div style="color: var(--text-muted);">No data available</div>';
1573
+
1574
+ // Problem resources
1575
+ const problemProviders = providers.filter(p => p.status !== 'validated').slice(0, 5);
1576
+ document.getElementById('problem-resources').innerHTML = problemProviders.map(p => `
1577
+ <div style="padding: 12px; background: rgba(239, 68, 68, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--danger);">
1578
+ <strong>${p.name}</strong>
1579
+ <div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">${p.provider_id}</div>
1580
+ <div style="font-size: 12px; color: var(--danger); margin-top: 4px;">Status: ${p.status}</div>
1581
+ </div>
1582
+ `).join('') || '<div style="color: var(--text-muted);">No issues detected ✅</div>';
1583
+ }
1584
+ } catch (error) {
1585
+ console.error('Error loading analytics:', error);
1586
+ }
1587
+ }
1588
+
1589
+ // Load Logs
1590
+ async function loadLogs() {
1591
+ try {
1592
+ const response = await fetch('/api/logs/recent');
1593
+ if (response.ok) {
1594
+ const data = await response.json();
1595
+ const logs = data.logs || [];
1596
+
1597
+ const container = document.getElementById('logs-container');
1598
+ if (logs.length === 0) {
1599
+ container.innerHTML = '<div style="color: var(--text-muted);">No logs available</div>';
1600
+ return;
1601
+ }
1602
+
1603
+ container.innerHTML = logs.map(log => `
1604
+ <div style="padding: 8px; border-bottom: 1px solid var(--border); animation: slideIn 0.3s;">
1605
+ <span style="color: var(--text-muted);">[${log.timestamp || 'N/A'}]</span>
1606
+ <span style="color: ${log.level === 'ERROR' ? 'var(--danger)' : 'var(--text-light)'};">${log.message || JSON.stringify(log)}</span>
1607
+ </div>
1608
+ `).join('');
1609
+ } else {
1610
+ document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Failed to load logs</div>';
1611
+ }
1612
+ } catch (error) {
1613
+ console.error('Error loading logs:', error);
1614
+ document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Error loading logs: ' + error.message + '</div>';
1615
+ }
1616
+ }
1617
+
1618
+ // Load Discovery Stats
1619
+ async function loadDiscoveryStats() {
1620
+ try {
1621
+ const response = await fetch('/api/apl/summary');
1622
+ if (response.ok) {
1623
+ const data = await response.json();
1624
+ document.getElementById('discovery-found').textContent = data.total_active_providers || 0;
1625
+ document.getElementById('discovery-validated').textContent = (data.http_valid || 0) + (data.hf_valid || 0);
1626
+ document.getElementById('discovery-failed').textContent = (data.http_invalid || 0) + (data.hf_invalid || 0);
1627
+
1628
+ if (data.timestamp) {
1629
+ document.getElementById('discovery-last').textContent = new Date(data.timestamp).toLocaleTimeString();
1630
+ }
1631
+ }
1632
+ } catch (error) {
1633
+ console.error('Error loading discovery stats:', error);
1634
+ }
1635
+ }
1636
+
1637
+ // Run Full Discovery
1638
+ async function runFullDiscovery() {
1639
+ const btn = document.getElementById('discovery-btn');
1640
+ btn.disabled = true;
1641
+ btn.textContent = '⏳ Discovering...';
1642
+
1643
+ document.getElementById('discovery-progress').style.display = 'block';
1644
+
1645
+ try {
1646
+ let progress = 0;
1647
+ const progressInterval = setInterval(() => {
1648
+ progress += 5;
1649
+ if (progress <= 95) {
1650
+ document.getElementById('discovery-progress-bar').style.width = progress + '%';
1651
+ document.getElementById('discovery-percent').textContent = progress + '%';
1652
+ }
1653
+ }, 200);
1654
+
1655
+ const response = await fetch('/api/apl/run', { method: 'POST' });
1656
+
1657
+ clearInterval(progressInterval);
1658
+ document.getElementById('discovery-progress-bar').style.width = '100%';
1659
+ document.getElementById('discovery-percent').textContent = '100%';
1660
+
1661
+ if (response.ok) {
1662
+ const result = await response.json();
1663
+ showToast('Discovery completed successfully!', 'success');
1664
+ loadDiscoveryStats();
1665
+ } else {
1666
+ showToast('Discovery failed', 'error');
1667
+ }
1668
+ } catch (error) {
1669
+ console.error('Error during discovery:', error);
1670
+ showToast('Error: ' + error.message, 'error');
1671
+ } finally {
1672
+ btn.disabled = false;
1673
+ btn.textContent = '🚀 Run Full Discovery';
1674
+ setTimeout(() => {
1675
+ document.getElementById('discovery-progress').style.display = 'none';
1676
+ }, 2000);
1677
+ }
1678
+ }
1679
+
1680
+ // Run APL Scan
1681
+ async function runAPLScan() {
1682
+ showToast('Running APL scan...', 'info');
1683
+
1684
+ try {
1685
+ const response = await fetch('/api/apl/run', { method: 'POST' });
1686
+
1687
+ if (response.ok) {
1688
+ showToast('APL scan completed!', 'success');
1689
+ loadDiscoveryStats();
1690
+ loadDashboardData();
1691
+ } else {
1692
+ showToast('APL scan failed', 'error');
1693
+ }
1694
+ } catch (error) {
1695
+ console.error('Error running APL:', error);
1696
+ showToast('Error: ' + error.message, 'error');
1697
+ }
1698
+ }
1699
+
1700
+ // Run Diagnostics
1701
+ async function runDiagnostics(autoFix) {
1702
+ showToast('Running diagnostics...', 'info');
1703
+
1704
+ try {
1705
+ const response = await fetch(`/api/diagnostics/run?auto_fix=${autoFix}`, { method: 'POST' });
1706
+
1707
+ if (response.ok) {
1708
+ const result = await response.json();
1709
+
1710
+ let html = `
1711
+ <div class="card" style="background: rgba(16, 185, 129, 0.1); margin-top: 20px;">
1712
+ <h3>Diagnostics Results</h3>
1713
+ <p><strong>Issues Found:</strong> ${result.issues_found || 0}</p>
1714
+ <p><strong>Status:</strong> ${result.status || 'completed'}</p>
1715
+ ${autoFix ? `<p><strong>Fixes Applied:</strong> ${result.fixes_applied?.length || 0}</p>` : ''}
1716
+ </div>
1717
+ `;
1718
+
1719
+ document.getElementById('diagnostics-output').innerHTML = html;
1720
+ showToast('Diagnostics completed', 'success');
1721
+ } else {
1722
+ showToast('Diagnostics failed', 'error');
1723
+ }
1724
+ } catch (error) {
1725
+ console.error('Error running diagnostics:', error);
1726
+ showToast('Error: ' + error.message, 'error');
1727
+ }
1728
+ }
1729
+
1730
+ // Utility Functions
1731
+ function showToast(message, type = 'info') {
1732
+ const toast = document.getElementById('toast');
1733
+ const toastMessage = document.getElementById('toast-message');
1734
+
1735
+ toast.className = `toast ${type}`;
1736
+ toastMessage.textContent = message;
1737
+ toast.classList.add('show');
1738
+
1739
+ setTimeout(() => {
1740
+ toast.classList.remove('show');
1741
+ }, 3000);
1742
+ }
1743
+
1744
+ function refreshAllData() {
1745
+ showToast('Refreshing all data...', 'info');
1746
+ loadDashboardData();
1747
+ loadResources();
1748
+ }
1749
+
1750
+ function refreshAnalytics() {
1751
+ showToast('Refreshing analytics...', 'info');
1752
+ loadAnalytics();
1753
+ }
1754
+
1755
+ function refreshLogs() {
1756
+ loadLogs();
1757
+ }
1758
+
1759
+ function filterLogs() {
1760
+ loadLogs();
1761
+ }
1762
+
1763
+ function scanResources() {
1764
+ showToast('Scanning resources...', 'info');
1765
+ loadResources();
1766
+ }
1767
+
1768
+ function fixDuplicates() {
1769
+ if (!confirm('Remove duplicate resources?')) return;
1770
+ showToast('Removing duplicates...', 'info');
1771
+ }
1772
+
1773
+ function openAddResourceModal() {
1774
+ document.getElementById('add-resource-modal').classList.add('show');
1775
+ }
1776
+
1777
+ function closeAddResourceModal() {
1778
+ document.getElementById('add-resource-modal').classList.remove('show');
1779
+ }
1780
+
1781
+ async function addResource() {
1782
+ showToast('Adding resource...', 'info');
1783
+ closeAddResourceModal();
1784
+ }
1785
+
1786
+ function testResource(id) {
1787
+ showToast(`Testing resource: ${id}`, 'info');
1788
+ }
1789
+
1790
+ function editResource(id) {
1791
+ showToast(`Edit resource: ${id}`, 'info');
1792
+ }
1793
+
1794
+ async function removeResource(id) {
1795
+ if (!confirm(`Remove resource: ${id}?`)) return;
1796
+ showToast('Resource removed', 'success');
1797
+ loadResources();
1798
+ }
1799
+
1800
+ function validateAllResources() {
1801
+ showToast('Validating all resources...', 'info');
1802
+ }
1803
+
1804
+ function refreshAllResources() {
1805
+ loadResources();
1806
+ }
1807
+
1808
+ function removeInvalidResources() {
1809
+ if (!confirm('Remove all invalid resources?')) return;
1810
+ showToast('Removing invalid resources...', 'info');
1811
+ }
1812
+
1813
+ function exportResources() {
1814
+ showToast('Exporting configuration...', 'info');
1815
+ }
1816
+
1817
+ function importResources() {
1818
+ showToast('Import configuration...', 'info');
1819
+ }
1820
+
1821
+ function exportAnalytics() {
1822
+ showToast('Exporting analytics...', 'info');
1823
+ }
1824
+
1825
+ function exportLogs() {
1826
+ showToast('Exporting logs...', 'info');
1827
+ }
1828
+
1829
+ function clearLogs() {
1830
+ if (!confirm('Clear all logs?')) return;
1831
+ showToast('Logs cleared', 'success');
1832
+ }
1833
+
1834
+ function testConnections() {
1835
+ showToast('Testing connections...', 'info');
1836
+ }
1837
+
1838
+ function clearCache() {
1839
+ if (!confirm('Clear cache?')) return;
1840
+ showToast('Cache cleared', 'success');
1841
+ }
1842
+
1843
+ function discoverHFModels() {
1844
+ runFullDiscovery();
1845
+ }
1846
+
1847
+ function discoverAPIs() {
1848
+ runFullDiscovery();
1849
+ }
1850
+
1851
+ // Auto-refresh
1852
+ function startAutoRefresh() {
1853
+ setInterval(() => {
1854
+ const activeTab = document.querySelector('.tab-content.active').id;
1855
+ if (activeTab === 'tab-dashboard') {
1856
+ loadDashboardData();
1857
+ }
1858
+ }, 30000);
1859
+ }
1860
+ </script>
1861
+ </body>
1862
+ </html>
archive_html/admin_improved.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Provider Telemetry Console</title>
7
+ <link rel="stylesheet" href="static/css/pro-dashboard.css" />
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ </head>
10
+ <body data-theme="dark">
11
+ <main class="main-area" style="margin-left:auto;margin-right:auto;max-width:1400px;">
12
+ <header class="topbar">
13
+ <div>
14
+ <h1>Provider Monitoring</h1>
15
+ <p class="text-muted">Glass dashboard for ingestion partners</p>
16
+ </div>
17
+ <div class="status-group">
18
+ <div class="status-pill" data-admin-health data-state="warn">
19
+ <span class="status-dot"></span>
20
+ <span>checking</span>
21
+ </div>
22
+ <button class="ghost" data-admin-refresh>Refresh</button>
23
+ </div>
24
+ </header>
25
+ <section class="page active">
26
+ <div class="stats-grid" data-admin-providers></div>
27
+ <div class="grid-two">
28
+ <div class="glass-card">
29
+ <h3>Latency Distribution</h3>
30
+ <canvas id="provider-latency-chart" height="220"></canvas>
31
+ </div>
32
+ <div class="glass-card">
33
+ <h3>Health Split</h3>
34
+ <canvas id="provider-status-chart" height="220"></canvas>
35
+ </div>
36
+ </div>
37
+ <div class="glass-card">
38
+ <div class="section-header">
39
+ <h3>Provider Directory</h3>
40
+ <span class="text-muted">Fetched from /api/providers</span>
41
+ </div>
42
+ <div class="table-wrapper">
43
+ <table>
44
+ <thead>
45
+ <tr>
46
+ <th>Name</th>
47
+ <th>Category</th>
48
+ <th>Latency</th>
49
+ <th>Status</th>
50
+ <th>Endpoint</th>
51
+ </tr>
52
+ </thead>
53
+ <tbody data-admin-table></tbody>
54
+ </table>
55
+ </div>
56
+ </div>
57
+ </section>
58
+ </main>
59
+ <script type="module" src="static/js/adminDashboard.js"></script>
60
+ </body>
61
+ </html>
archive_html/admin_pro.html ADDED
@@ -0,0 +1,657 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Pro Dashboard</title>
7
+
8
+ <!-- Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
11
+
12
+ <!-- Chart.js -->
13
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
14
+
15
+ <!-- Design System CSS -->
16
+ <link rel="stylesheet" href="static/css/design-tokens.css" />
17
+ <link rel="stylesheet" href="static/css/glassmorphism.css" />
18
+ <link rel="stylesheet" href="static/css/design-system.css" />
19
+ <link rel="stylesheet" href="static/css/components.css" />
20
+ <link rel="stylesheet" href="static/css/dashboard.css" />
21
+ <link rel="stylesheet" href="static/css/pro-dashboard.css" />
22
+
23
+ <style>
24
+ /* Enhanced Combobox Styles */
25
+ .combobox-wrapper {
26
+ position: relative;
27
+ width: 100%;
28
+ }
29
+
30
+ .combobox-input {
31
+ width: 100%;
32
+ padding: var(--space-3) var(--space-10) var(--space-3) var(--space-4);
33
+ background: var(--input-bg);
34
+ border: 1px solid var(--border-light);
35
+ border-radius: var(--radius-sm);
36
+ color: var(--text-strong);
37
+ font-family: var(--font-main);
38
+ font-size: var(--fs-base);
39
+ backdrop-filter: var(--blur-md);
40
+ transition: all var(--transition-fast);
41
+ }
42
+
43
+ .combobox-input:focus {
44
+ outline: none;
45
+ border-color: var(--brand-cyan);
46
+ box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.30), var(--glow-cyan);
47
+ background: rgba(15, 23, 42, 0.80);
48
+ }
49
+
50
+ .combobox-icon {
51
+ position: absolute;
52
+ right: var(--space-4);
53
+ top: 50%;
54
+ transform: translateY(-50%);
55
+ pointer-events: none;
56
+ color: var(--text-muted);
57
+ }
58
+
59
+ .combobox-dropdown {
60
+ position: absolute;
61
+ top: calc(100% + var(--space-2));
62
+ left: 0;
63
+ right: 0;
64
+ max-height: 320px;
65
+ overflow-y: auto;
66
+ background: var(--surface-glass-strong);
67
+ border: 1px solid var(--border-medium);
68
+ border-radius: var(--radius-md);
69
+ backdrop-filter: var(--blur-xl);
70
+ box-shadow: var(--shadow-xl);
71
+ z-index: var(--z-dropdown);
72
+ display: none;
73
+ }
74
+
75
+ .combobox-dropdown.active {
76
+ display: block;
77
+ animation: dropdown-fade-in 0.2s ease-out;
78
+ }
79
+
80
+ @keyframes dropdown-fade-in {
81
+ from {
82
+ opacity: 0;
83
+ transform: translateY(-8px);
84
+ }
85
+ to {
86
+ opacity: 1;
87
+ transform: translateY(0);
88
+ }
89
+ }
90
+
91
+ .combobox-option {
92
+ padding: var(--space-3) var(--space-4);
93
+ display: flex;
94
+ align-items: center;
95
+ gap: var(--space-3);
96
+ cursor: pointer;
97
+ transition: all var(--transition-fast);
98
+ border-bottom: 1px solid var(--border-subtle);
99
+ }
100
+
101
+ .combobox-option:last-child {
102
+ border-bottom: none;
103
+ }
104
+
105
+ .combobox-option:hover {
106
+ background: rgba(6, 182, 212, 0.15);
107
+ border-left: 3px solid var(--brand-cyan);
108
+ }
109
+
110
+ .combobox-option.selected {
111
+ background: rgba(6, 182, 212, 0.20);
112
+ border-left: 3px solid var(--brand-cyan);
113
+ }
114
+
115
+ .combobox-option-icon {
116
+ width: 32px;
117
+ height: 32px;
118
+ border-radius: 50%;
119
+ flex-shrink: 0;
120
+ }
121
+
122
+ .combobox-option-text {
123
+ flex: 1;
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: var(--space-1);
127
+ }
128
+
129
+ .combobox-option-name {
130
+ font-weight: var(--fw-semibold);
131
+ color: var(--text-strong);
132
+ }
133
+
134
+ .combobox-option-symbol {
135
+ font-size: var(--fs-xs);
136
+ color: var(--text-muted);
137
+ text-transform: uppercase;
138
+ }
139
+
140
+ .combobox-option-price {
141
+ font-size: var(--fs-sm);
142
+ font-weight: var(--fw-medium);
143
+ color: var(--text-soft);
144
+ }
145
+
146
+ /* Dynamic Sidebar Stats */
147
+ .sidebar-stats {
148
+ margin-top: auto;
149
+ padding: var(--space-4);
150
+ background: rgba(255, 255, 255, 0.03);
151
+ border-radius: var(--radius-md);
152
+ border: 1px solid var(--border-subtle);
153
+ }
154
+
155
+ .sidebar-stat-item {
156
+ display: flex;
157
+ justify-content: space-between;
158
+ align-items: center;
159
+ padding: var(--space-2) 0;
160
+ border-bottom: 1px solid var(--border-subtle);
161
+ }
162
+
163
+ .sidebar-stat-item:last-child {
164
+ border-bottom: none;
165
+ }
166
+
167
+ .sidebar-stat-label {
168
+ font-size: var(--fs-xs);
169
+ color: var(--text-muted);
170
+ font-weight: var(--fw-medium);
171
+ }
172
+
173
+ .sidebar-stat-value {
174
+ font-size: var(--fs-sm);
175
+ font-weight: var(--fw-semibold);
176
+ color: var(--text-strong);
177
+ }
178
+
179
+ .sidebar-stat-value.positive {
180
+ color: var(--success);
181
+ }
182
+
183
+ .sidebar-stat-value.negative {
184
+ color: var(--danger);
185
+ }
186
+
187
+ /* Enhanced Chart Container */
188
+ .chart-controls {
189
+ display: grid;
190
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
191
+ gap: var(--space-4);
192
+ margin-bottom: var(--space-6);
193
+ padding: var(--space-5);
194
+ background: var(--surface-glass);
195
+ border: 1px solid var(--border-light);
196
+ border-radius: var(--radius-lg);
197
+ backdrop-filter: var(--blur-lg);
198
+ }
199
+
200
+ .chart-control-group {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: var(--space-2);
204
+ }
205
+
206
+ .chart-control-label {
207
+ font-size: var(--fs-sm);
208
+ font-weight: var(--fw-semibold);
209
+ color: var(--text-normal);
210
+ display: flex;
211
+ align-items: center;
212
+ gap: var(--space-2);
213
+ }
214
+
215
+ .chart-button-group {
216
+ display: flex;
217
+ gap: var(--space-2);
218
+ flex-wrap: wrap;
219
+ }
220
+
221
+ .chart-button {
222
+ flex: 1;
223
+ min-width: 80px;
224
+ padding: var(--space-2) var(--space-3);
225
+ background: var(--surface-glass);
226
+ border: 1px solid var(--border-light);
227
+ border-radius: var(--radius-sm);
228
+ color: var(--text-soft);
229
+ font-size: var(--fs-sm);
230
+ font-weight: var(--fw-medium);
231
+ cursor: pointer;
232
+ transition: all var(--transition-fast);
233
+ }
234
+
235
+ .chart-button:hover {
236
+ background: var(--surface-glass-strong);
237
+ border-color: var(--brand-cyan);
238
+ color: var(--text-strong);
239
+ }
240
+
241
+ .chart-button.active {
242
+ background: var(--gradient-primary);
243
+ border-color: transparent;
244
+ color: white;
245
+ box-shadow: var(--glow-cyan);
246
+ }
247
+
248
+ /* Color Scheme Selector */
249
+ .color-scheme-selector {
250
+ display: flex;
251
+ gap: var(--space-2);
252
+ }
253
+
254
+ .color-scheme-option {
255
+ width: 40px;
256
+ height: 40px;
257
+ border-radius: var(--radius-sm);
258
+ border: 2px solid var(--border-light);
259
+ cursor: pointer;
260
+ transition: all var(--transition-fast);
261
+ position: relative;
262
+ }
263
+
264
+ .color-scheme-option:hover {
265
+ transform: scale(1.1);
266
+ border-color: var(--brand-cyan);
267
+ }
268
+
269
+ .color-scheme-option.active {
270
+ border-color: var(--brand-cyan);
271
+ box-shadow: var(--glow-cyan);
272
+ }
273
+
274
+ .color-scheme-option.active::after {
275
+ content: '✓';
276
+ position: absolute;
277
+ top: 50%;
278
+ left: 50%;
279
+ transform: translate(-50%, -50%);
280
+ color: white;
281
+ font-weight: bold;
282
+ font-size: 18px;
283
+ }
284
+
285
+ .color-scheme-blue {
286
+ background: linear-gradient(135deg, #3B82F6, #06B6D4);
287
+ }
288
+
289
+ .color-scheme-purple {
290
+ background: linear-gradient(135deg, #8B5CF6, #EC4899);
291
+ }
292
+
293
+ .color-scheme-green {
294
+ background: linear-gradient(135deg, #10B981, #34D399);
295
+ }
296
+
297
+ .color-scheme-orange {
298
+ background: linear-gradient(135deg, #F97316, #FBBF24);
299
+ }
300
+
301
+ .color-scheme-rainbow {
302
+ background: linear-gradient(135deg, #3B82F6, #8B5CF6, #EC4899, #F97316);
303
+ }
304
+ </style>
305
+ </head>
306
+ <body data-theme="dark">
307
+
308
+ <script>
309
+ // Backend Configuration
310
+ if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
311
+ window.BACKEND_URL = `http://${window.location.hostname}:7860`;
312
+ } else {
313
+ window.BACKEND_URL = 'https://really-amin-datasourceforcryptocurrency.hf.space';
314
+ }
315
+ </script>
316
+
317
+ <div class="app-shell">
318
+ <!-- Dynamic Sidebar -->
319
+ <aside class="sidebar" id="dynamicSidebar">
320
+ <div class="brand">
321
+ <div class="brand-icon">
322
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none">
323
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5"/>
324
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5"/>
325
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5"/>
326
+ </svg>
327
+ </div>
328
+ <div class="brand-text">
329
+ <strong>Crypto Intelligence</strong>
330
+ <span class="env-pill">
331
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none">
332
+ <circle cx="12" cy="12" r="3" fill="currentColor"/>
333
+ </svg>
334
+ Pro Edition
335
+ </span>
336
+ </div>
337
+ </div>
338
+
339
+ <nav class="nav" id="mainNav">
340
+ <button class="nav-button active" data-nav="page-overview">
341
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
342
+ <path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
343
+ </svg>
344
+ Overview
345
+ </button>
346
+ <button class="nav-button" data-nav="page-chart">
347
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
348
+ <path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/>
349
+ <path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/>
350
+ </svg>
351
+ Advanced Charts
352
+ </button>
353
+ <button class="nav-button" data-nav="page-compare">
354
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
355
+ <path d="M3 17l6-6 4 4 8-8" stroke="currentColor" stroke-width="2"/>
356
+ </svg>
357
+ Compare Coins
358
+ </button>
359
+ <button class="nav-button" data-nav="page-portfolio">
360
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
361
+ <path d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2"/>
362
+ </svg>
363
+ Portfolio
364
+ </button>
365
+ </nav>
366
+
367
+ <!-- Dynamic Stats -->
368
+ <div class="sidebar-stats" id="sidebarStats">
369
+ <div class="sidebar-stat-item">
370
+ <span class="sidebar-stat-label">Market Cap</span>
371
+ <span class="sidebar-stat-value" id="sidebarMarketCap">Loading...</span>
372
+ </div>
373
+ <div class="sidebar-stat-item">
374
+ <span class="sidebar-stat-label">24h Volume</span>
375
+ <span class="sidebar-stat-value" id="sidebarVolume">Loading...</span>
376
+ </div>
377
+ <div class="sidebar-stat-item">
378
+ <span class="sidebar-stat-label">BTC Price</span>
379
+ <span class="sidebar-stat-value positive" id="sidebarBTC">Loading...</span>
380
+ </div>
381
+ <div class="sidebar-stat-item">
382
+ <span class="sidebar-stat-label">ETH Price</span>
383
+ <span class="sidebar-stat-value positive" id="sidebarETH">Loading...</span>
384
+ </div>
385
+ </div>
386
+
387
+ <div class="sidebar-footer">
388
+ <div class="footer-badge">
389
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
390
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
391
+ <path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
392
+ </svg>
393
+ <span id="lastUpdate">Just now</span>
394
+ </div>
395
+ </div>
396
+ </aside>
397
+
398
+ <!-- Main Content -->
399
+ <main class="main-area">
400
+ <!-- Top Bar -->
401
+ <header class="topbar">
402
+ <div class="topbar-content">
403
+ <div class="topbar-icon">
404
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none">
405
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5"/>
406
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5"/>
407
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5"/>
408
+ </svg>
409
+ </div>
410
+ <div class="topbar-text">
411
+ <h1>
412
+ <span class="title-gradient">Professional</span>
413
+ <span class="title-accent">Dashboard</span>
414
+ </h1>
415
+ <p class="text-muted">
416
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="display: inline-block; vertical-align: middle; margin-right: 6px;">
417
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
418
+ <path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
419
+ </svg>
420
+ Real-time market data with advanced analytics
421
+ </p>
422
+ </div>
423
+ </div>
424
+ <div class="status-group">
425
+ <div class="status-pill" data-state="ok">
426
+ <span class="status-dot"></span>
427
+ <span class="status-label">API Connected</span>
428
+ </div>
429
+ <div class="status-pill" data-state="ok">
430
+ <span class="status-dot"></span>
431
+ <span class="status-label">Live Data</span>
432
+ </div>
433
+ </div>
434
+ </header>
435
+
436
+ <div class="page-container">
437
+ <!-- Overview Page -->
438
+ <section id="page-overview" class="page active">
439
+ <div class="section-header">
440
+ <h2 class="section-title">Market Overview</h2>
441
+ <span class="chip">Real-time</span>
442
+ </div>
443
+
444
+ <!-- Stats Grid -->
445
+ <div class="stats-grid" id="statsGrid">
446
+ <!-- Stats will be dynamically loaded -->
447
+ </div>
448
+
449
+ <!-- Main Chart -->
450
+ <div class="glass-card" style="margin-top: var(--space-6);">
451
+ <div class="card-header">
452
+ <h4 class="card-title">
453
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
454
+ <path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/>
455
+ <path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/>
456
+ </svg>
457
+ Market Trends - Top 10 Cryptocurrencies
458
+ </h4>
459
+ <div style="display: flex; gap: var(--space-2);">
460
+ <span class="badge badge-cyan">24H</span>
461
+ <button class="btn-secondary btn-sm" onclick="refreshData()">
462
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
463
+ <path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2"/>
464
+ <path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" stroke="currentColor" stroke-width="2"/>
465
+ </svg>
466
+ Refresh
467
+ </button>
468
+ </div>
469
+ </div>
470
+ <div class="chart-container" style="height: 450px;">
471
+ <canvas id="mainChart"></canvas>
472
+ </div>
473
+ </div>
474
+
475
+ <!-- Top Coins Table -->
476
+ <div class="glass-card" style="margin-top: var(--space-6);">
477
+ <div class="card-header">
478
+ <h4 class="card-title">
479
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
480
+ <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"/>
481
+ </svg>
482
+ Top Cryptocurrencies
483
+ </h4>
484
+ </div>
485
+ <div class="table-container">
486
+ <table class="table">
487
+ <thead>
488
+ <tr>
489
+ <th>#</th>
490
+ <th>Coin</th>
491
+ <th>Price</th>
492
+ <th>24h Change</th>
493
+ <th>7d Change</th>
494
+ <th>Market Cap</th>
495
+ <th>Volume (24h)</th>
496
+ <th>Last 7 Days</th>
497
+ </tr>
498
+ </thead>
499
+ <tbody id="topCoinsTable">
500
+ <!-- Data will be loaded dynamically -->
501
+ </tbody>
502
+ </table>
503
+ </div>
504
+ </div>
505
+ </section>
506
+
507
+ <!-- Advanced Charts Page -->
508
+ <section id="page-chart" class="page">
509
+ <div class="section-header">
510
+ <h2 class="section-title">Advanced Chart Analysis</h2>
511
+ <span class="chip">Interactive</span>
512
+ </div>
513
+
514
+ <!-- Chart Controls -->
515
+ <div class="chart-controls">
516
+ <div class="chart-control-group">
517
+ <label class="chart-control-label">
518
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
519
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
520
+ </svg>
521
+ Select Cryptocurrency
522
+ </label>
523
+ <div class="combobox-wrapper">
524
+ <input
525
+ type="text"
526
+ class="combobox-input"
527
+ id="coinSelector"
528
+ placeholder="Search for a coin..."
529
+ autocomplete="off"
530
+ />
531
+ <svg class="combobox-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
532
+ <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
533
+ </svg>
534
+ <div class="combobox-dropdown" id="coinDropdown">
535
+ <!-- Options will be loaded dynamically -->
536
+ </div>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="chart-control-group">
541
+ <label class="chart-control-label">
542
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
543
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
544
+ <path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
545
+ </svg>
546
+ Timeframe
547
+ </label>
548
+ <div class="chart-button-group">
549
+ <button class="chart-button" data-timeframe="1">1D</button>
550
+ <button class="chart-button active" data-timeframe="7">7D</button>
551
+ <button class="chart-button" data-timeframe="30">30D</button>
552
+ <button class="chart-button" data-timeframe="90">90D</button>
553
+ <button class="chart-button" data-timeframe="365">1Y</button>
554
+ </div>
555
+ </div>
556
+
557
+ <div class="chart-control-group">
558
+ <label class="chart-control-label">
559
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
560
+ <path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" stroke="currentColor" stroke-width="2"/>
561
+ </svg>
562
+ Color Scheme
563
+ </label>
564
+ <div class="color-scheme-selector">
565
+ <div class="color-scheme-option color-scheme-blue active" data-scheme="blue"></div>
566
+ <div class="color-scheme-option color-scheme-purple" data-scheme="purple"></div>
567
+ <div class="color-scheme-option color-scheme-green" data-scheme="green"></div>
568
+ <div class="color-scheme-option color-scheme-orange" data-scheme="orange"></div>
569
+ <div class="color-scheme-option color-scheme-rainbow" data-scheme="rainbow"></div>
570
+ </div>
571
+ </div>
572
+ </div>
573
+
574
+ <!-- Price Chart -->
575
+ <div class="glass-card">
576
+ <div class="card-header">
577
+ <h4 class="card-title" id="chartTitle">Bitcoin (BTC) Price Chart</h4>
578
+ <div style="display: flex; gap: var(--space-2); align-items: center;">
579
+ <span class="badge badge-success" id="chartPrice">$0</span>
580
+ <span class="badge badge-cyan" id="chartChange">0%</span>
581
+ </div>
582
+ </div>
583
+ <div class="chart-container" style="height: 500px;">
584
+ <canvas id="priceChart"></canvas>
585
+ </div>
586
+ </div>
587
+
588
+ <!-- Volume Chart -->
589
+ <div class="glass-card" style="margin-top: var(--space-6);">
590
+ <div class="card-header">
591
+ <h4 class="card-title">Trading Volume</h4>
592
+ </div>
593
+ <div class="chart-container" style="height: 300px;">
594
+ <canvas id="volumeChart"></canvas>
595
+ </div>
596
+ </div>
597
+ </section>
598
+
599
+ <!-- Compare Page -->
600
+ <section id="page-compare" class="page">
601
+ <div class="section-header">
602
+ <h2 class="section-title">Compare Cryptocurrencies</h2>
603
+ <span class="chip">Side by Side</span>
604
+ </div>
605
+
606
+ <div class="alert alert-info">
607
+ <svg class="alert-icon" width="20" height="20" viewBox="0 0 24 24" fill="none">
608
+ <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
609
+ <path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2"/>
610
+ </svg>
611
+ <div class="alert-content">
612
+ <div class="alert-title">Compare up to 5 cryptocurrencies</div>
613
+ <div class="alert-description">Select coins to compare their performance side by side</div>
614
+ </div>
615
+ </div>
616
+
617
+ <div class="glass-card" style="margin-top: var(--space-6);">
618
+ <div class="card-header">
619
+ <h4 class="card-title">Comparison Chart</h4>
620
+ </div>
621
+ <div class="chart-container" style="height: 450px;">
622
+ <canvas id="compareChart"></canvas>
623
+ </div>
624
+ </div>
625
+ </section>
626
+
627
+ <!-- Portfolio Page -->
628
+ <section id="page-portfolio" class="page">
629
+ <div class="section-header">
630
+ <h2 class="section-title">Portfolio Tracker</h2>
631
+ <button class="btn-primary">
632
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
633
+ <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2"/>
634
+ </svg>
635
+ Add Asset
636
+ </button>
637
+ </div>
638
+
639
+ <div class="empty-state">
640
+ <div class="empty-state-icon">📊</div>
641
+ <div class="empty-state-title">No Portfolio Data</div>
642
+ <div class="empty-state-description">
643
+ Start tracking your crypto portfolio by adding your first asset
644
+ </div>
645
+ <button class="btn-primary" style="margin-top: var(--space-4);">
646
+ Get Started
647
+ </button>
648
+ </div>
649
+ </section>
650
+ </div>
651
+ </main>
652
+ </div>
653
+
654
+ <!-- Load App JS -->
655
+ <script type="module" src="static/js/app-pro.js"></script>
656
+ </body>
657
+ </html>
archive_html/complete_dashboard.html ADDED
@@ -0,0 +1,857 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Complete Dashboard</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: #0f172a;
17
+ color: #e2e8f0;
18
+ overflow-x: hidden;
19
+ }
20
+
21
+ .header {
22
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
23
+ padding: 20px 40px;
24
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
25
+ position: sticky;
26
+ top: 0;
27
+ z-index: 1000;
28
+ }
29
+
30
+ .header-content {
31
+ max-width: 1600px;
32
+ margin: 0 auto;
33
+ display: flex;
34
+ justify-content: space-between;
35
+ align-items: center;
36
+ }
37
+
38
+ .logo {
39
+ font-size: 24px;
40
+ font-weight: bold;
41
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
42
+ -webkit-background-clip: text;
43
+ -webkit-text-fill-color: transparent;
44
+ display: flex;
45
+ align-items: center;
46
+ gap: 10px;
47
+ }
48
+
49
+ .header-actions {
50
+ display: flex;
51
+ gap: 15px;
52
+ align-items: center;
53
+ }
54
+
55
+ .btn {
56
+ padding: 10px 20px;
57
+ border: none;
58
+ border-radius: 8px;
59
+ font-weight: 600;
60
+ cursor: pointer;
61
+ transition: all 0.3s ease;
62
+ font-size: 14px;
63
+ }
64
+
65
+ .btn-primary {
66
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
67
+ color: white;
68
+ }
69
+
70
+ .btn-primary:hover {
71
+ transform: translateY(-2px);
72
+ box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
73
+ }
74
+
75
+ .btn-secondary {
76
+ background: #334155;
77
+ color: #e2e8f0;
78
+ }
79
+
80
+ .btn-secondary:hover {
81
+ background: #475569;
82
+ }
83
+
84
+ .container {
85
+ max-width: 1600px;
86
+ margin: 0 auto;
87
+ padding: 30px;
88
+ }
89
+
90
+ .tabs {
91
+ display: flex;
92
+ gap: 5px;
93
+ margin-bottom: 30px;
94
+ background: #1e293b;
95
+ padding: 10px;
96
+ border-radius: 12px;
97
+ overflow-x: auto;
98
+ }
99
+
100
+ .tab {
101
+ padding: 12px 24px;
102
+ border: none;
103
+ background: transparent;
104
+ color: #94a3b8;
105
+ cursor: pointer;
106
+ border-radius: 8px;
107
+ font-weight: 600;
108
+ transition: all 0.3s ease;
109
+ white-space: nowrap;
110
+ }
111
+
112
+ .tab:hover {
113
+ background: #334155;
114
+ color: #e2e8f0;
115
+ }
116
+
117
+ .tab.active {
118
+ background: linear-gradient(135deg, #3b82f6, #8b5cf6);
119
+ color: white;
120
+ }
121
+
122
+ .tab-content {
123
+ display: none;
124
+ }
125
+
126
+ .tab-content.active {
127
+ display: block;
128
+ animation: fadeIn 0.3s ease;
129
+ }
130
+
131
+ @keyframes fadeIn {
132
+ from { opacity: 0; transform: translateY(10px); }
133
+ to { opacity: 1; transform: translateY(0); }
134
+ }
135
+
136
+ .stats-grid {
137
+ display: grid;
138
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
139
+ gap: 20px;
140
+ margin-bottom: 30px;
141
+ }
142
+
143
+ .stat-card {
144
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
145
+ padding: 25px;
146
+ border-radius: 12px;
147
+ border: 1px solid #334155;
148
+ transition: all 0.3s ease;
149
+ }
150
+
151
+ .stat-card:hover {
152
+ transform: translateY(-5px);
153
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
154
+ border-color: #3b82f6;
155
+ }
156
+
157
+ .stat-card h3 {
158
+ color: #94a3b8;
159
+ font-size: 14px;
160
+ text-transform: uppercase;
161
+ margin-bottom: 10px;
162
+ font-weight: 600;
163
+ }
164
+
165
+ .stat-card .value {
166
+ font-size: 36px;
167
+ font-weight: bold;
168
+ margin-bottom: 5px;
169
+ }
170
+
171
+ .stat-card .label {
172
+ color: #64748b;
173
+ font-size: 13px;
174
+ }
175
+
176
+ .stat-card.green .value { color: #10b981; }
177
+ .stat-card.blue .value { color: #3b82f6; }
178
+ .stat-card.purple .value { color: #8b5cf6; }
179
+ .stat-card.orange .value { color: #f59e0b; }
180
+ .stat-card.red .value { color: #ef4444; }
181
+
182
+ .card {
183
+ background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
184
+ border-radius: 12px;
185
+ padding: 25px;
186
+ border: 1px solid #334155;
187
+ margin-bottom: 20px;
188
+ }
189
+
190
+ .card h2 {
191
+ color: #e2e8f0;
192
+ margin-bottom: 20px;
193
+ font-size: 20px;
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 10px;
197
+ }
198
+
199
+ .providers-grid {
200
+ display: grid;
201
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
202
+ gap: 15px;
203
+ max-height: 600px;
204
+ overflow-y: auto;
205
+ }
206
+
207
+ .provider-card {
208
+ background: #0f172a;
209
+ border-radius: 8px;
210
+ padding: 15px;
211
+ border-left: 4px solid #334155;
212
+ transition: all 0.3s ease;
213
+ }
214
+
215
+ .provider-card:hover {
216
+ transform: translateX(5px);
217
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
218
+ }
219
+
220
+ .provider-card.online {
221
+ border-left-color: #10b981;
222
+ background: linear-gradient(to right, rgba(16, 185, 129, 0.1), #0f172a);
223
+ }
224
+ .provider-card.offline {
225
+ border-left-color: #ef4444;
226
+ background: linear-gradient(to right, rgba(239, 68, 68, 0.1), #0f172a);
227
+ }
228
+ .provider-card.degraded {
229
+ border-left-color: #f59e0b;
230
+ background: linear-gradient(to right, rgba(245, 158, 11, 0.1), #0f172a);
231
+ }
232
+
233
+ .provider-card .name {
234
+ font-weight: 600;
235
+ color: #e2e8f0;
236
+ margin-bottom: 8px;
237
+ font-size: 15px;
238
+ }
239
+
240
+ .provider-card .category {
241
+ font-size: 12px;
242
+ color: #94a3b8;
243
+ margin-bottom: 8px;
244
+ }
245
+
246
+ .provider-card .status {
247
+ display: inline-block;
248
+ padding: 4px 10px;
249
+ border-radius: 6px;
250
+ font-size: 11px;
251
+ font-weight: 600;
252
+ text-transform: uppercase;
253
+ }
254
+
255
+ .status.online { background: rgba(16, 185, 129, 0.2); color: #10b981; }
256
+ .status.offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
257
+ .status.degraded { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
258
+
259
+ .provider-card .response-time {
260
+ font-size: 12px;
261
+ color: #64748b;
262
+ margin-top: 8px;
263
+ }
264
+
265
+ .category-list {
266
+ display: grid;
267
+ gap: 15px;
268
+ }
269
+
270
+ .category-item {
271
+ background: #0f172a;
272
+ border-radius: 8px;
273
+ padding: 20px;
274
+ border-left: 4px solid #3b82f6;
275
+ }
276
+
277
+ .category-item .name {
278
+ font-weight: 600;
279
+ color: #e2e8f0;
280
+ margin-bottom: 12px;
281
+ font-size: 16px;
282
+ }
283
+
284
+ .category-item .stats {
285
+ display: flex;
286
+ gap: 20px;
287
+ font-size: 14px;
288
+ }
289
+
290
+ .category-item .stat {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 6px;
294
+ }
295
+
296
+ .loading {
297
+ text-align: center;
298
+ padding: 60px;
299
+ color: #64748b;
300
+ font-size: 16px;
301
+ }
302
+
303
+ .spinner {
304
+ border: 3px solid #334155;
305
+ border-top: 3px solid #3b82f6;
306
+ border-radius: 50%;
307
+ width: 40px;
308
+ height: 40px;
309
+ animation: spin 1s linear infinite;
310
+ margin: 20px auto;
311
+ }
312
+
313
+ @keyframes spin {
314
+ 0% { transform: rotate(0deg); }
315
+ 100% { transform: rotate(360deg); }
316
+ }
317
+
318
+ .grid-2 {
319
+ display: grid;
320
+ grid-template-columns: 2fr 1fr;
321
+ gap: 20px;
322
+ }
323
+
324
+ .market-item {
325
+ display: flex;
326
+ justify-content: space-between;
327
+ align-items: center;
328
+ padding: 15px;
329
+ background: #0f172a;
330
+ border-radius: 8px;
331
+ margin-bottom: 10px;
332
+ border: 1px solid #334155;
333
+ }
334
+
335
+ .market-item:hover {
336
+ border-color: #3b82f6;
337
+ }
338
+
339
+ .market-item .coin-info {
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 15px;
343
+ }
344
+
345
+ .market-item .coin-name {
346
+ font-weight: 600;
347
+ color: #e2e8f0;
348
+ }
349
+
350
+ .market-item .coin-symbol {
351
+ color: #94a3b8;
352
+ font-size: 14px;
353
+ }
354
+
355
+ .market-item .price {
356
+ font-size: 18px;
357
+ font-weight: 600;
358
+ color: #3b82f6;
359
+ }
360
+
361
+ .market-item .change {
362
+ padding: 4px 10px;
363
+ border-radius: 6px;
364
+ font-weight: 600;
365
+ font-size: 14px;
366
+ }
367
+
368
+ .market-item .change.positive {
369
+ background: rgba(16, 185, 129, 0.2);
370
+ color: #10b981;
371
+ }
372
+
373
+ .market-item .change.negative {
374
+ background: rgba(239, 68, 68, 0.2);
375
+ color: #ef4444;
376
+ }
377
+
378
+ .search-box {
379
+ width: 100%;
380
+ padding: 12px 20px;
381
+ background: #0f172a;
382
+ border: 1px solid #334155;
383
+ border-radius: 8px;
384
+ color: #e2e8f0;
385
+ font-size: 14px;
386
+ margin-bottom: 20px;
387
+ }
388
+
389
+ .search-box:focus {
390
+ outline: none;
391
+ border-color: #3b82f6;
392
+ }
393
+
394
+ ::-webkit-scrollbar {
395
+ width: 8px;
396
+ height: 8px;
397
+ }
398
+
399
+ ::-webkit-scrollbar-track {
400
+ background: #1e293b;
401
+ }
402
+
403
+ ::-webkit-scrollbar-thumb {
404
+ background: #334155;
405
+ border-radius: 4px;
406
+ }
407
+
408
+ ::-webkit-scrollbar-thumb:hover {
409
+ background: #475569;
410
+ }
411
+
412
+ @media (max-width: 768px) {
413
+ .grid-2 {
414
+ grid-template-columns: 1fr;
415
+ }
416
+ .stats-grid {
417
+ grid-template-columns: 1fr;
418
+ }
419
+ .header-content {
420
+ flex-direction: column;
421
+ gap: 15px;
422
+ }
423
+ }
424
+
425
+ .status-indicator {
426
+ display: inline-block;
427
+ width: 8px;
428
+ height: 8px;
429
+ border-radius: 50%;
430
+ margin-right: 8px;
431
+ }
432
+
433
+ .status-indicator.online { background: #10b981; }
434
+ .status-indicator.offline { background: #ef4444; }
435
+ .status-indicator.degraded { background: #f59e0b; }
436
+
437
+ .empty-state {
438
+ text-align: center;
439
+ padding: 60px 20px;
440
+ color: #64748b;
441
+ }
442
+
443
+ .empty-state h3 {
444
+ color: #94a3b8;
445
+ margin-bottom: 10px;
446
+ }
447
+ </style>
448
+ </head>
449
+ <body>
450
+ <div class="header">
451
+ <div class="header-content">
452
+ <div class="logo">
453
+ <span>🚀</span>
454
+ <span>Crypto API Monitor</span>
455
+ </div>
456
+ <div class="header-actions">
457
+ <span id="connectionStatus" style="color: #64748b; font-size: 14px;">
458
+ <span class="status-indicator online"></span>
459
+ Connected
460
+ </span>
461
+ <button class="btn btn-secondary" onclick="refreshData()">🔄 Refresh</button>
462
+ <button class="btn btn-primary" onclick="exportData()">💾 Export</button>
463
+ </div>
464
+ </div>
465
+ </div>
466
+
467
+ <div class="container">
468
+ <div class="tabs">
469
+ <button class="tab active" onclick="switchTab('overview')">📊 Overview</button>
470
+ <button class="tab" onclick="switchTab('providers')">🔌 Providers</button>
471
+ <button class="tab" onclick="switchTab('categories')">📁 Categories</button>
472
+ <button class="tab" onclick="switchTab('market')">💰 Market Data</button>
473
+ <button class="tab" onclick="switchTab('health')">❤️ Health</button>
474
+ </div>
475
+
476
+ <!-- Overview Tab -->
477
+ <div id="overview" class="tab-content active">
478
+ <div class="stats-grid">
479
+ <div class="stat-card blue">
480
+ <h3>Total Providers</h3>
481
+ <div class="value" id="totalProviders">-</div>
482
+ <div class="label">API Sources</div>
483
+ </div>
484
+ <div class="stat-card green">
485
+ <h3>Online</h3>
486
+ <div class="value" id="onlineProviders">-</div>
487
+ <div class="label">Working Perfectly</div>
488
+ </div>
489
+ <div class="stat-card orange">
490
+ <h3>Degraded</h3>
491
+ <div class="value" id="degradedProviders">-</div>
492
+ <div class="label">Slow Response</div>
493
+ </div>
494
+ <div class="stat-card red">
495
+ <h3>Offline</h3>
496
+ <div class="value" id="offlineProviders">-</div>
497
+ <div class="label">Not Responding</div>
498
+ </div>
499
+ </div>
500
+
501
+ <div class="grid-2">
502
+ <div class="card">
503
+ <h2>🔌 Recent Provider Status</h2>
504
+ <div id="recentProviders">
505
+ <div class="loading">
506
+ <div class="spinner"></div>
507
+ Loading providers...
508
+ </div>
509
+ </div>
510
+ </div>
511
+
512
+ <div class="card">
513
+ <h2>📈 System Health</h2>
514
+ <div id="systemHealth">
515
+ <div class="loading">
516
+ <div class="spinner"></div>
517
+ Loading health data...
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+ </div>
523
+
524
+ <!-- Providers Tab -->
525
+ <div id="providers" class="tab-content">
526
+ <div class="card">
527
+ <h2>🔌 All Providers</h2>
528
+ <input type="text" class="search-box" id="providerSearch" placeholder="Search providers..." onkeyup="filterProviders()">
529
+ <div class="providers-grid" id="allProviders">
530
+ <div class="loading">
531
+ <div class="spinner"></div>
532
+ Loading providers...
533
+ </div>
534
+ </div>
535
+ </div>
536
+ </div>
537
+
538
+ <!-- Categories Tab -->
539
+ <div id="categories" class="tab-content">
540
+ <div class="card">
541
+ <h2>📁 Categories Breakdown</h2>
542
+ <div class="category-list" id="categoriesList">
543
+ <div class="loading">
544
+ <div class="spinner"></div>
545
+ Loading categories...
546
+ </div>
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ <!-- Market Tab -->
552
+ <div id="market" class="tab-content">
553
+ <div class="card">
554
+ <h2>💰 Market Data</h2>
555
+ <div id="marketData">
556
+ <div class="loading">
557
+ <div class="spinner"></div>
558
+ Loading market data...
559
+ </div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+
564
+ <!-- Health Tab -->
565
+ <div id="health" class="tab-content">
566
+ <div class="stats-grid">
567
+ <div class="stat-card purple">
568
+ <h3>Uptime</h3>
569
+ <div class="value" id="uptimePercent">-</div>
570
+ <div class="label">Overall Health</div>
571
+ </div>
572
+ <div class="stat-card blue">
573
+ <h3>Avg Response</h3>
574
+ <div class="value" id="avgResponse">-</div>
575
+ <div class="label">Milliseconds</div>
576
+ </div>
577
+ <div class="stat-card green">
578
+ <h3>Categories</h3>
579
+ <div class="value" id="totalCategories">-</div>
580
+ <div class="label">Data Types</div>
581
+ </div>
582
+ <div class="stat-card orange">
583
+ <h3>Last Check</h3>
584
+ <div class="value" id="lastCheck" style="font-size: 18px;">-</div>
585
+ <div class="label">Timestamp</div>
586
+ </div>
587
+ </div>
588
+
589
+ <div class="card">
590
+ <h2>📊 Detailed Health Report</h2>
591
+ <div id="healthDetails">
592
+ <div class="loading">
593
+ <div class="spinner"></div>
594
+ Loading health details...
595
+ </div>
596
+ </div>
597
+ </div>
598
+ </div>
599
+ </div>
600
+
601
+ <script>
602
+ let allProvidersData = [];
603
+ let currentTab = 'overview';
604
+
605
+ function switchTab(tabName) {
606
+ // Hide all tabs
607
+ document.querySelectorAll('.tab-content').forEach(tab => {
608
+ tab.classList.remove('active');
609
+ });
610
+ document.querySelectorAll('.tab').forEach(tab => {
611
+ tab.classList.remove('active');
612
+ });
613
+
614
+ // Show selected tab
615
+ document.getElementById(tabName).classList.add('active');
616
+ event.target.classList.add('active');
617
+ currentTab = tabName;
618
+
619
+ // Load data for specific tabs
620
+ if (tabName === 'market') {
621
+ loadMarketData();
622
+ }
623
+ }
624
+
625
+ async function loadProviders() {
626
+ try {
627
+ const response = await fetch('/api/providers');
628
+ const providers = await response.json();
629
+ allProvidersData = providers;
630
+
631
+ // Calculate stats
632
+ const online = providers.filter(p => p.status === 'online').length;
633
+ const offline = providers.filter(p => p.status === 'offline').length;
634
+ const degraded = providers.filter(p => p.status === 'degraded').length;
635
+ const uptime = ((online / providers.length) * 100).toFixed(1);
636
+
637
+ // Update stats
638
+ document.getElementById('totalProviders').textContent = providers.length;
639
+ document.getElementById('onlineProviders').textContent = online;
640
+ document.getElementById('degradedProviders').textContent = degraded;
641
+ document.getElementById('offlineProviders').textContent = offline;
642
+ document.getElementById('uptimePercent').textContent = uptime + '%';
643
+
644
+ // Calculate average response time
645
+ const responseTimes = providers.filter(p => p.response_time_ms).map(p => p.response_time_ms);
646
+ const avgResp = responseTimes.length > 0 ?
647
+ Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0;
648
+ document.getElementById('avgResponse').textContent = avgResp + 'ms';
649
+
650
+ // Group by category
651
+ const categories = {};
652
+ providers.forEach(p => {
653
+ if (!categories[p.category]) {
654
+ categories[p.category] = { total: 0, online: 0, offline: 0, degraded: 0 };
655
+ }
656
+ categories[p.category].total++;
657
+ categories[p.category][p.status]++;
658
+ });
659
+
660
+ document.getElementById('totalCategories').textContent = Object.keys(categories).length;
661
+ document.getElementById('lastCheck').textContent = new Date().toLocaleTimeString();
662
+
663
+ // Display recent providers (first 10)
664
+ displayRecentProviders(providers.slice(0, 10));
665
+
666
+ // Display all providers
667
+ displayAllProviders(providers);
668
+
669
+ // Display categories
670
+ displayCategories(categories);
671
+
672
+ // Display health details
673
+ displayHealthDetails(providers);
674
+
675
+ } catch (error) {
676
+ console.error('Error loading providers:', error);
677
+ }
678
+ }
679
+
680
+ function displayRecentProviders(providers) {
681
+ const html = providers.map(p => `
682
+ <div class="provider-card ${p.status}">
683
+ <div class="name">${p.name}</div>
684
+ <div class="category">${p.category}</div>
685
+ <span class="status ${p.status}">${p.status}</span>
686
+ ${p.response_time_ms ? `<div class="response-time">${Math.round(p.response_time_ms)}ms</div>` : ''}
687
+ </div>
688
+ `).join('');
689
+ document.getElementById('recentProviders').innerHTML = `<div class="providers-grid" style="max-height: 400px;">${html}</div>`;
690
+ }
691
+
692
+ function displayAllProviders(providers) {
693
+ const html = providers.map(p => `
694
+ <div class="provider-card ${p.status}" data-name="${p.name.toLowerCase()}" data-category="${p.category.toLowerCase()}">
695
+ <div class="name">${p.name}</div>
696
+ <div class="category">${p.category}</div>
697
+ <span class="status ${p.status}">${p.status}</span>
698
+ ${p.response_time_ms ? `<div class="response-time">⚡ ${Math.round(p.response_time_ms)}ms</div>` : ''}
699
+ </div>
700
+ `).join('');
701
+ document.getElementById('allProviders').innerHTML = html;
702
+ }
703
+
704
+ function displayCategories(categories) {
705
+ const html = Object.entries(categories).map(([name, stats]) => `
706
+ <div class="category-item">
707
+ <div class="name">${name}</div>
708
+ <div class="stats">
709
+ <div class="stat">
710
+ <span class="status-indicator online"></span>
711
+ <span>${stats.online} online</span>
712
+ </div>
713
+ <div class="stat">
714
+ <span class="status-indicator degraded"></span>
715
+ <span>${stats.degraded} degraded</span>
716
+ </div>
717
+ <div class="stat">
718
+ <span class="status-indicator offline"></span>
719
+ <span>${stats.offline} offline</span>
720
+ </div>
721
+ <div class="stat" style="margin-left: auto; color: #3b82f6; font-weight: 600;">
722
+ ${stats.total} total
723
+ </div>
724
+ </div>
725
+ </div>
726
+ `).join('');
727
+ document.getElementById('categoriesList').innerHTML = html;
728
+ }
729
+
730
+ function displayHealthDetails(providers) {
731
+ const online = providers.filter(p => p.status === 'online');
732
+ const degraded = providers.filter(p => p.status === 'degraded');
733
+ const offline = providers.filter(p => p.status === 'offline');
734
+
735
+ const html = `
736
+ <div style="display: grid; gap: 20px;">
737
+ <div class="category-item" style="border-left-color: #10b981;">
738
+ <div class="name">✅ Online Providers (${online.length})</div>
739
+ <div style="color: #94a3b8; margin-top: 10px;">
740
+ ${online.slice(0, 5).map(p => p.name).join(', ')}
741
+ ${online.length > 5 ? ` and ${online.length - 5} more...` : ''}
742
+ </div>
743
+ </div>
744
+ <div class="category-item" style="border-left-color: #f59e0b;">
745
+ <div class="name">⚠️ Degraded Providers (${degraded.length})</div>
746
+ <div style="color: #94a3b8; margin-top: 10px;">
747
+ ${degraded.length > 0 ? degraded.map(p => p.name).join(', ') : 'None'}
748
+ </div>
749
+ </div>
750
+ <div class="category-item" style="border-left-color: #ef4444;">
751
+ <div class="name">❌ Offline Providers (${offline.length})</div>
752
+ <div style="color: #94a3b8; margin-top: 10px;">
753
+ ${offline.length > 0 ? offline.map(p => p.name).join(', ') : 'None'}
754
+ </div>
755
+ </div>
756
+ </div>
757
+ `;
758
+ document.getElementById('healthDetails').innerHTML = html;
759
+
760
+ const systemHealthHtml = `
761
+ <div style="display: grid; gap: 15px;">
762
+ <div style="padding: 15px; background: #0f172a; border-radius: 8px;">
763
+ <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">System Status</div>
764
+ <div style="font-size: 24px; font-weight: 600; color: ${online.length >= providers.length * 0.7 ? '#10b981' : '#f59e0b'};">
765
+ ${online.length >= providers.length * 0.7 ? '✅ Healthy' : '⚠️ Degraded'}
766
+ </div>
767
+ </div>
768
+ <div style="padding: 15px; background: #0f172a; border-radius: 8px;">
769
+ <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Availability</div>
770
+ <div style="font-size: 24px; font-weight: 600; color: #3b82f6;">
771
+ ${((online.length / providers.length) * 100).toFixed(1)}%
772
+ </div>
773
+ </div>
774
+ <div style="padding: 15px; background: #0f172a; border-radius: 8px;">
775
+ <div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Total Checks</div>
776
+ <div style="font-size: 24px; font-weight: 600; color: #8b5cf6;">
777
+ ${providers.length}
778
+ </div>
779
+ </div>
780
+ </div>
781
+ `;
782
+ document.getElementById('systemHealth').innerHTML = systemHealthHtml;
783
+ }
784
+
785
+ async function loadMarketData() {
786
+ try {
787
+ const response = await fetch('/api/market');
788
+ const data = await response.json();
789
+
790
+ if (data.cryptocurrencies && data.cryptocurrencies.length > 0) {
791
+ const html = data.cryptocurrencies.map(coin => `
792
+ <div class="market-item">
793
+ <div class="coin-info">
794
+ <div>
795
+ <div class="coin-name">${coin.name}</div>
796
+ <div class="coin-symbol">${coin.symbol}</div>
797
+ </div>
798
+ </div>
799
+ <div style="display: flex; align-items: center; gap: 20px;">
800
+ <div class="price">$${coin.price.toLocaleString()}</div>
801
+ <div class="change ${coin.change_24h >= 0 ? 'positive' : 'negative'}">
802
+ ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%
803
+ </div>
804
+ </div>
805
+ </div>
806
+ `).join('');
807
+ document.getElementById('marketData').innerHTML = html;
808
+ } else {
809
+ document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>No market data available</h3><p>Market data providers may be offline</p></div>';
810
+ }
811
+ } catch (error) {
812
+ console.error('Error loading market data:', error);
813
+ document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>Error loading market data</h3></div>';
814
+ }
815
+ }
816
+
817
+ function filterProviders() {
818
+ const search = document.getElementById('providerSearch').value.toLowerCase();
819
+ const cards = document.querySelectorAll('#allProviders .provider-card');
820
+
821
+ cards.forEach(card => {
822
+ const name = card.getAttribute('data-name');
823
+ const category = card.getAttribute('data-category');
824
+ if (name.includes(search) || category.includes(search)) {
825
+ card.style.display = 'block';
826
+ } else {
827
+ card.style.display = 'none';
828
+ }
829
+ });
830
+ }
831
+
832
+ function refreshData() {
833
+ loadProviders();
834
+ if (currentTab === 'market') {
835
+ loadMarketData();
836
+ }
837
+ }
838
+
839
+ function exportData() {
840
+ const dataStr = JSON.stringify(allProvidersData, null, 2);
841
+ const dataBlob = new Blob([dataStr], {type: 'application/json'});
842
+ const url = URL.createObjectURL(dataBlob);
843
+ const link = document.createElement('a');
844
+ link.href = url;
845
+ link.download = `crypto-monitor-${new Date().toISOString()}.json`;
846
+ link.click();
847
+ }
848
+
849
+ // Load data on start
850
+ loadProviders();
851
+
852
+ // Auto-refresh every 30 seconds
853
+ setInterval(refreshData, 30000);
854
+ </script>
855
+ </body>
856
+ </html>
857
+
archive_html/crypto_dashboard_pro.html ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Console</title>
7
+ <link rel="stylesheet" href="static/css/pro-dashboard.css" />
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ </head>
10
+ <body data-theme="dark">
11
+ <div class="app-shell">
12
+ <aside class="sidebar">
13
+ <div class="brand">
14
+ <strong>CRYPTO DT</strong>
15
+ <span class="env-pill">
16
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
17
+ <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
18
+ <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
19
+ <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
20
+ </svg>
21
+ HF Space
22
+ </span>
23
+ </div>
24
+ <nav class="nav">
25
+ <button class="nav-button active" data-nav="page-overview">
26
+ <svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
27
+ Overview
28
+ </button>
29
+ <button class="nav-button" data-nav="page-market">
30
+ <svg viewBox="0 0 24 24"><path d="M3 17h2v-7H3v7zm4 0h2V7H7v10zm4 0h2V4h-2v13zm4 0h2V9h-2v8zm4 0h2V2h-2v15z"/></svg>
31
+ Market Intelligence
32
+ </button>
33
+ <button class="nav-button" data-nav="page-news">
34
+ <svg viewBox="0 0 24 24"><path d="M21 6h-4V4a2 2 0 0 0-2-2H5C3.897 2 3 2.897 3 4v14a2 2 0 0 0 2 2h13a3 3 0 0 0 3-3V6zm-6-2v14H5V4h10zm4 16a1 1 0 0 1-1 1h-1V8h2v12z"/></svg>
35
+ News & Sentiment
36
+ </button>
37
+ <button class="nav-button" data-nav="page-chart">
38
+ <svg viewBox="0 0 24 24"><path d="M5 3H3v18h18v-2H5z"/><path d="M7 15l3-3 4 4 6-6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
39
+ Chart Lab
40
+ </button>
41
+ <button class="nav-button" data-nav="page-ai">
42
+ <svg viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
43
+ AI Advisor
44
+ </button>
45
+ <button class="nav-button" data-nav="page-datasets">
46
+ <svg viewBox="0 0 24 24"><path d="M4 5v14l8 4 8-4V5l-8-4-8 4zm8-2.18L17.74 6 12 8.82 6.26 6 12 2.82zM6 8.97l6 2.82 6-2.82v3.06l-6 2.82-6-2.82V8.97zm0 5.03l6 2.82 6-2.82v3.2L12 20l-6-2.8v-3.2z"/></svg>
47
+ Datasets & Models
48
+ </button>
49
+ <button class="nav-button" data-nav="page-debug">
50
+ <svg viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm4 0h2v-2H7v2zm4 0h2v-2h-2v2zm4 0h2v-2h-2v2zm4-2v2h2v-2h-2z"/><path d="M5 5h14v4H5zm0 10h14v4H5z"/></svg>
51
+ System Health
52
+ </button>
53
+ <button class="nav-button" data-nav="page-settings">
54
+ <svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.61-.22l-2.39.96a7.03 7.03 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.5.42l-.36 2.54a7.03 7.03 0 0 0-1.63.94l-2.39-.96a.5.5 0 0 0-.61.22L2.69 8.53a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32c.14.24.43.34.68.22l2.39-.96c.5.4 1.05.72 1.63.94l.36 2.54c.04.26.25.42.5.42h3.8c.25 0 .46-.16.5-.42l.36-2.54c.58-.22 1.13-.54 1.63-.94l2.39.96c.25.12.54.02.68-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z"/></svg>
55
+ Settings
56
+ </button>
57
+ </nav>
58
+ <div class="sidebar-footer">
59
+ Unified crypto intelligence console<br />Designed for HF Spaces
60
+ </div>
61
+ </aside>
62
+ <main class="main-area">
63
+ <header class="topbar">
64
+ <div>
65
+ <h1>Professional Intelligence Dashboard</h1>
66
+ <p class="text-muted">Real-time analytics, AI insights, and provider telemetry</p>
67
+ </div>
68
+ <div class="status-group">
69
+ <div class="status-pill" data-api-health data-state="warn">
70
+ <span class="status-dot"></span>
71
+ <span>checking</span>
72
+ </div>
73
+ <div class="status-pill" data-ws-status data-state="warn">
74
+ <span class="status-dot"></span>
75
+ <span>connecting</span>
76
+ </div>
77
+ </div>
78
+ </header>
79
+ <div class="page-container">
80
+ <section id="page-overview" class="page active">
81
+ <div class="section-header">
82
+ <h2 class="section-title">Global Overview</h2>
83
+ <span class="chip">Updated live from /api/market/stats</span>
84
+ </div>
85
+ <div class="stats-grid" data-overview-stats></div>
86
+ <div class="grid-two">
87
+ <div class="glass-card">
88
+ <div class="section-header">
89
+ <h3>Top Coins</h3>
90
+ <span class="text-muted">Top performers by market cap</span>
91
+ </div>
92
+ <div class="table-wrapper">
93
+ <table>
94
+ <thead>
95
+ <tr>
96
+ <th>#</th>
97
+ <th>Symbol</th>
98
+ <th>Name</th>
99
+ <th>Price</th>
100
+ <th>24h %</th>
101
+ <th>Volume</th>
102
+ <th>Market Cap</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody data-top-coins-body></tbody>
106
+ </table>
107
+ </div>
108
+ </div>
109
+ <div class="glass-card">
110
+ <div class="section-header">
111
+ <h3>Global Sentiment</h3>
112
+ <span class="text-muted">Powered by CryptoBERT stack</span>
113
+ </div>
114
+ <canvas id="sentiment-chart" height="220"></canvas>
115
+ </div>
116
+ </div>
117
+ </section>
118
+
119
+ <section id="page-market" class="page">
120
+ <div class="section-header">
121
+ <h2 class="section-title">Market Intelligence</h2>
122
+ <div class="controls-bar">
123
+ <div class="input-chip">
124
+ <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>
125
+ <input type="text" placeholder="Search symbol" data-market-search />
126
+ </div>
127
+ <div class="input-chip">
128
+ Timeframe:
129
+ <button class="ghost" data-timeframe="1d">1D</button>
130
+ <button class="ghost active" data-timeframe="7d">7D</button>
131
+ <button class="ghost" data-timeframe="30d">30D</button>
132
+ </div>
133
+ <label class="input-chip"> Live updates
134
+ <div class="toggle">
135
+ <input type="checkbox" data-live-toggle />
136
+ <span></span>
137
+ </div>
138
+ </label>
139
+ </div>
140
+ </div>
141
+ <div class="glass-card">
142
+ <div class="table-wrapper">
143
+ <table>
144
+ <thead>
145
+ <tr>
146
+ <th>#</th>
147
+ <th>Symbol</th>
148
+ <th>Name</th>
149
+ <th>Price</th>
150
+ <th>24h %</th>
151
+ <th>Volume</th>
152
+ <th>Market Cap</th>
153
+ </tr>
154
+ </thead>
155
+ <tbody data-market-body></tbody>
156
+ </table>
157
+ </div>
158
+ </div>
159
+ <div class="drawer" data-market-drawer>
160
+ <button class="ghost" data-close-drawer>Close</button>
161
+ <h3 data-drawer-symbol>—</h3>
162
+ <div data-drawer-stats></div>
163
+ <div class="glass-card" data-chart-wrapper>
164
+ <canvas id="market-detail-chart" height="180"></canvas>
165
+ </div>
166
+ <div class="glass-card">
167
+ <h4>Latest Headlines</h4>
168
+ <div data-drawer-news></div>
169
+ </div>
170
+ </div>
171
+ </section>
172
+
173
+ <section id="page-news" class="page">
174
+ <div class="section-header">
175
+ <h2 class="section-title">News & Sentiment</h2>
176
+ </div>
177
+ <div class="controls-bar">
178
+ <select data-news-range>
179
+ <option value="24h">Last 24h</option>
180
+ <option value="7d">7 Days</option>
181
+ <option value="30d">30 Days</option>
182
+ </select>
183
+ <input type="text" placeholder="Search headline" data-news-search />
184
+ <input type="text" placeholder="Filter symbol (e.g. BTC)" data-news-symbol />
185
+ </div>
186
+ <div class="glass-card">
187
+ <div class="table-wrapper">
188
+ <table>
189
+ <thead>
190
+ <tr>
191
+ <th>Time</th>
192
+ <th>Source</th>
193
+ <th>Title</th>
194
+ <th>Symbols</th>
195
+ <th>Sentiment</th>
196
+ <th>Impact</th>
197
+ </tr>
198
+ </thead>
199
+ <tbody data-news-body></tbody>
200
+ </table>
201
+ </div>
202
+ </div>
203
+ <div class="modal-backdrop" data-news-modal>
204
+ <div class="modal">
205
+ <button class="ghost" data-close-news-modal>Close</button>
206
+ <div data-news-modal-content></div>
207
+ </div>
208
+ </div>
209
+ </section>
210
+
211
+ <section id="page-chart" class="page">
212
+ <div class="section-header">
213
+ <h2 class="section-title">Chart & Pattern Analysis</h2>
214
+ <div class="controls-bar">
215
+ <select data-chart-symbol>
216
+ <option value="BTC">BTC</option>
217
+ <option value="ETH">ETH</option>
218
+ <option value="SOL">SOL</option>
219
+ </select>
220
+ <div>
221
+ <button class="ghost active" data-chart-timeframe="7d">7D</button>
222
+ <button class="ghost" data-chart-timeframe="30d">30D</button>
223
+ <button class="ghost" data-chart-timeframe="90d">90D</button>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ <div class="glass-card">
228
+ <canvas id="chart-lab-canvas" height="240"></canvas>
229
+ </div>
230
+ <div class="glass-card">
231
+ <h4>Indicators</h4>
232
+ <div class="controls-bar">
233
+ <label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
234
+ <label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
235
+ <label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
236
+ <label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
237
+ </div>
238
+ <button class="primary" data-run-analysis>Run AI Analysis</button>
239
+ <div data-ai-insights class="ai-insights"></div>
240
+ </div>
241
+ </section>
242
+
243
+ <section id="page-ai" class="page">
244
+ <div class="section-header">
245
+ <h2 class="section-title">AI Trade Advisor</h2>
246
+ </div>
247
+ <div class="glass-card">
248
+ <form data-ai-form class="ai-form">
249
+ <div class="grid-two">
250
+ <label>Symbol
251
+ <select name="symbol">
252
+ <option value="BTC">BTC</option>
253
+ <option value="ETH">ETH</option>
254
+ <option value="SOL">SOL</option>
255
+ </select>
256
+ </label>
257
+ <label>Time Horizon
258
+ <select name="horizon">
259
+ <option value="intraday">Intraday</option>
260
+ <option value="swing">Swing</option>
261
+ <option value="long">Long Term</option>
262
+ </select>
263
+ </label>
264
+ <label>Risk Profile
265
+ <select name="risk">
266
+ <option value="conservative">Conservative</option>
267
+ <option value="moderate">Moderate</option>
268
+ <option value="aggressive">Aggressive</option>
269
+ </select>
270
+ </label>
271
+ <label>Context
272
+ <textarea name="context" placeholder="Optional prompts for the advisor"></textarea>
273
+ </label>
274
+ </div>
275
+ <button class="primary" type="submit">Generate AI Advice</button>
276
+ </form>
277
+ <div data-ai-result class="ai-result"></div>
278
+ <div class="inline-message inline-info" data-ai-disclaimer>
279
+ This is experimental AI research, not financial advice.
280
+ </div>
281
+ </div>
282
+ </section>
283
+
284
+ <section id="page-datasets" class="page">
285
+ <div class="section-header">
286
+ <h2 class="section-title">Datasets & Models Lab</h2>
287
+ </div>
288
+ <div class="grid-two">
289
+ <div class="glass-card">
290
+ <h3>Datasets</h3>
291
+ <div class="table-wrapper">
292
+ <table>
293
+ <thead>
294
+ <tr>
295
+ <th>Name</th>
296
+ <th>Type</th>
297
+ <th>Updated</th>
298
+ <th>Preview</th>
299
+ </tr>
300
+ </thead>
301
+ <tbody data-datasets-body></tbody>
302
+ </table>
303
+ </div>
304
+ </div>
305
+ <div class="glass-card">
306
+ <h3>Models</h3>
307
+ <div class="table-wrapper">
308
+ <table>
309
+ <thead>
310
+ <tr>
311
+ <th>Name</th>
312
+ <th>Task</th>
313
+ <th>Status</th>
314
+ <th>Notes</th>
315
+ </tr>
316
+ </thead>
317
+ <tbody data-models-body></tbody>
318
+ </table>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ <div class="glass-card">
323
+ <h4>Test a Model</h4>
324
+ <form data-model-test-form class="grid-two">
325
+ <label>Model
326
+ <select data-model-select name="model"></select>
327
+ </label>
328
+ <label>Input
329
+ <textarea name="input" placeholder="Type a prompt"></textarea>
330
+ </label>
331
+ <button class="primary" type="submit">Run Test</button>
332
+ </form>
333
+ <div data-model-test-output></div>
334
+ </div>
335
+ <div class="modal-backdrop" data-dataset-modal>
336
+ <div class="modal">
337
+ <button class="ghost" data-close-dataset-modal>Close</button>
338
+ <div data-dataset-modal-content></div>
339
+ </div>
340
+ </div>
341
+ </section>
342
+
343
+ <section id="page-debug" class="page">
344
+ <div class="section-header">
345
+ <h2 class="section-title">System Health & Debug Console</h2>
346
+ <button class="ghost" data-refresh-health>Refresh</button>
347
+ </div>
348
+ <div class="stats-grid">
349
+ <div class="glass-card">
350
+ <h3>API Health</h3>
351
+ <div class="stat-value" data-health-status>—</div>
352
+ </div>
353
+ <div class="glass-card">
354
+ <h3>Providers</h3>
355
+ <div data-providers class="grid-two"></div>
356
+ </div>
357
+ </div>
358
+ <div class="grid-two">
359
+ <div class="glass-card">
360
+ <h4>Request Log</h4>
361
+ <div class="table-wrapper log-table">
362
+ <table>
363
+ <thead>
364
+ <tr>
365
+ <th>Time</th>
366
+ <th>Method</th>
367
+ <th>Endpoint</th>
368
+ <th>Status</th>
369
+ <th>Latency</th>
370
+ </tr>
371
+ </thead>
372
+ <tbody data-request-log></tbody>
373
+ </table>
374
+ </div>
375
+ </div>
376
+ <div class="glass-card">
377
+ <h4>Error Log</h4>
378
+ <div class="table-wrapper log-table">
379
+ <table>
380
+ <thead>
381
+ <tr>
382
+ <th>Time</th>
383
+ <th>Endpoint</th>
384
+ <th>Message</th>
385
+ </tr>
386
+ </thead>
387
+ <tbody data-error-log></tbody>
388
+ </table>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ <div class="glass-card">
393
+ <h4>WebSocket Events</h4>
394
+ <div class="table-wrapper log-table">
395
+ <table>
396
+ <thead>
397
+ <tr>
398
+ <th>Time</th>
399
+ <th>Type</th>
400
+ <th>Detail</th>
401
+ </tr>
402
+ </thead>
403
+ <tbody data-ws-log></tbody>
404
+ </table>
405
+ </div>
406
+ </div>
407
+ </section>
408
+
409
+ <section id="page-settings" class="page">
410
+ <div class="section-header">
411
+ <h2 class="section-title">Settings</h2>
412
+ </div>
413
+ <div class="glass-card">
414
+ <div class="grid-two">
415
+ <label class="input-chip">Light Theme
416
+ <div class="toggle">
417
+ <input type="checkbox" data-theme-toggle />
418
+ <span></span>
419
+ </div>
420
+ </label>
421
+ <label>Market Refresh (sec)
422
+ <input type="number" min="15" step="5" data-market-interval />
423
+ </label>
424
+ <label>News Refresh (sec)
425
+ <input type="number" min="30" step="10" data-news-interval />
426
+ </label>
427
+ <label class="input-chip">Compact Layout
428
+ <div class="toggle">
429
+ <input type="checkbox" data-layout-toggle />
430
+ <span></span>
431
+ </div>
432
+ </label>
433
+ </div>
434
+ </div>
435
+ </section>
436
+ </div>
437
+ </main>
438
+ </div>
439
+ <script type="module" src="static/js/app.js"></script>
440
+ </body>
441
+ </html>
archive_html/dashboard.html ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" />
6
+ <title>Crypto Intelligence Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
+ <link rel="stylesheet" href="/static/css/components.css" />
12
+ <script defer src="/static/js/ui-feedback.js"></script>
13
+ <script defer src="/static/js/dashboard-app.js"></script>
14
+ </head>
15
+ <body class="page page-dashboard">
16
+ <header class="top-nav">
17
+ <div class="branding">
18
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
19
+ <div>
20
+ <strong>Crypto Intelligence Hub</strong>
21
+ <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">Real-time data + HF models</small>
22
+ </div>
23
+ </div>
24
+ <nav class="nav-links">
25
+ <a class="active" href="/dashboard">Dashboard</a>
26
+ <a href="/admin">Admin</a>
27
+ <a href="/hf_console">HF Console</a>
28
+ <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
+ </nav>
30
+ </header>
31
+
32
+ <main class="page-content">
33
+ <section class="card" id="intro-card">
34
+ <div class="section-heading">
35
+ <h2>Unified Market Pulse</h2>
36
+ <span class="badge info" id="intro-source">Loading...</span>
37
+ </div>
38
+ <p style="color:var(--text-muted);max-width:780px;line-height:1.6;">
39
+ Live collectors + local fallback registry guarantee resilient insights. All numbers below already honor the FastAPI routes
40
+ (<code>/api/crypto/prices/top</code>, <code>/api/crypto/market-overview</code>, <code>/health</code>) so you can monitor status even when providers degrade.
41
+ </p>
42
+ </section>
43
+
44
+ <section class="card-grid" id="market-metrics">
45
+ <article class="card"><h3>Total Market Cap</h3><div class="metric-value" id="metric-market-cap">-</div><div class="metric-subtext" id="metric-cap-source"></div></article>
46
+ <article class="card"><h3>24h Volume</h3><div class="metric-value" id="metric-volume">-</div><div class="metric-subtext" id="metric-volume-source"></div></article>
47
+ <article class="card"><h3>BTC Dominance</h3><div class="metric-value" id="metric-btc-dom">-</div><div class="metric-subtext">Based on /api/crypto/market-overview</div></article>
48
+ <article class="card"><h3>System Health</h3><div class="metric-value" id="metric-health">-</div><div class="metric-subtext" id="metric-health-details"></div></article>
49
+ </section>
50
+
51
+ <section class="card table-card">
52
+ <div class="section-heading">
53
+ <h2>Top Assets</h2>
54
+ <span class="badge info" id="top-prices-source">Loading...</span>
55
+ </div>
56
+ <div class="table-wrapper">
57
+ <table>
58
+ <thead>
59
+ <tr><th>Symbol</th><th>Price</th><th>24h %</th><th>Volume</th></tr>
60
+ </thead>
61
+ <tbody id="top-prices-table">
62
+ <tr><td colspan="4">Loading...</td></tr>
63
+ </tbody>
64
+ </table>
65
+ </div>
66
+ </section>
67
+
68
+ <section class="split-grid">
69
+ <article class="card" id="overview-card">
70
+ <div class="section-heading">
71
+ <h2>Market Overview</h2>
72
+ <span class="badge info" id="market-overview-source">Loading...</span>
73
+ </div>
74
+ <ul class="list" id="market-overview-list"></ul>
75
+ </article>
76
+ <article class="card" id="system-card">
77
+ <div class="section-heading">
78
+ <h2>System & Rate Limits</h2>
79
+ <span class="badge info" id="system-status-source">/health</span>
80
+ </div>
81
+ <div id="system-health-status" class="metric-subtext"></div>
82
+ <ul class="list" id="system-status-list"></ul>
83
+ <div class="section-heading" style="margin-top:24px;">
84
+ <h2>Configuration</h2>
85
+ </div>
86
+ <ul class="list" id="system-config-list"></ul>
87
+ <div class="section-heading" style="margin-top:24px;">
88
+ <h2>Rate Limits</h2>
89
+ </div>
90
+ <ul class="list" id="rate-limits-list"></ul>
91
+ </article>
92
+ </section>
93
+
94
+ <section class="split-grid">
95
+ <article class="card" id="hf-widget">
96
+ <div class="section-heading">
97
+ <h2>HuggingFace Snapshot</h2>
98
+ <span class="badge info" id="hf-health-status">Loading...</span>
99
+ </div>
100
+ <div id="hf-widget-summary" class="metric-subtext"></div>
101
+ <ul class="list" id="hf-registry-list"></ul>
102
+ </article>
103
+ <article class="card">
104
+ <div class="section-heading">
105
+ <h2>Live Stream (/ws)</h2>
106
+ <span class="badge info" id="ws-status">Connecting...</span>
107
+ </div>
108
+ <div class="ws-stream" id="ws-stream"></div>
109
+ </article>
110
+ </section>
111
+ </main>
112
+ </body>
113
+ </html>
archive_html/dashboard_standalone.html ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Monitor - Provider Dashboard</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ :root {
15
+ --primary: #6366f1;
16
+ --primary-dark: #4f46e5;
17
+ --success: #10b981;
18
+ --warning: #f59e0b;
19
+ --danger: #ef4444;
20
+ --info: #3b82f6;
21
+ --bg-dark: #0f172a;
22
+ --bg-card: #1e293b;
23
+ --bg-hover: #334155;
24
+ --text-light: #f1f5f9;
25
+ --text-muted: #94a3b8;
26
+ --border: #334155;
27
+ }
28
+
29
+ body {
30
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
31
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
32
+ color: var(--text-light);
33
+ line-height: 1.6;
34
+ min-height: 100vh;
35
+ padding: 20px;
36
+ }
37
+
38
+ .container {
39
+ max-width: 1600px;
40
+ margin: 0 auto;
41
+ }
42
+
43
+ header {
44
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
45
+ padding: 30px;
46
+ border-radius: 16px;
47
+ margin-bottom: 30px;
48
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
49
+ text-align: center;
50
+ }
51
+
52
+ header h1 {
53
+ font-size: 32px;
54
+ font-weight: 700;
55
+ margin-bottom: 8px;
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: center;
59
+ gap: 12px;
60
+ }
61
+
62
+ .stats-grid {
63
+ display: grid;
64
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
65
+ gap: 20px;
66
+ margin-bottom: 30px;
67
+ }
68
+
69
+ .stat-card {
70
+ background: var(--bg-card);
71
+ padding: 24px;
72
+ border-radius: 12px;
73
+ border: 1px solid var(--border);
74
+ border-top: 4px solid var(--primary);
75
+ }
76
+
77
+ .stat-card:hover {
78
+ transform: translateY(-4px);
79
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
80
+ transition: all 0.3s;
81
+ }
82
+
83
+ .stat-card .label {
84
+ color: var(--text-muted);
85
+ font-size: 13px;
86
+ text-transform: uppercase;
87
+ letter-spacing: 0.5px;
88
+ font-weight: 600;
89
+ margin-bottom: 8px;
90
+ }
91
+
92
+ .stat-card .value {
93
+ font-size: 36px;
94
+ font-weight: 700;
95
+ margin: 8px 0;
96
+ }
97
+
98
+ .filters {
99
+ background: var(--bg-card);
100
+ padding: 20px;
101
+ border-radius: 12px;
102
+ margin-bottom: 20px;
103
+ border: 1px solid var(--border);
104
+ display: flex;
105
+ gap: 15px;
106
+ flex-wrap: wrap;
107
+ }
108
+
109
+ .search-box input, .filters select, .filters button {
110
+ padding: 10px 16px;
111
+ background: var(--bg-dark);
112
+ border: 1px solid var(--border);
113
+ border-radius: 8px;
114
+ color: var(--text-light);
115
+ font-size: 14px;
116
+ }
117
+
118
+ .filters button {
119
+ background: var(--primary);
120
+ border-color: var(--primary);
121
+ cursor: pointer;
122
+ font-weight: 600;
123
+ }
124
+
125
+ .filters button:hover {
126
+ background: var(--primary-dark);
127
+ }
128
+
129
+ .table-container {
130
+ background: var(--bg-card);
131
+ border-radius: 12px;
132
+ border: 1px solid var(--border);
133
+ overflow: hidden;
134
+ }
135
+
136
+ table {
137
+ width: 100%;
138
+ border-collapse: collapse;
139
+ }
140
+
141
+ thead {
142
+ background: var(--bg-dark);
143
+ }
144
+
145
+ thead th {
146
+ text-align: left;
147
+ padding: 16px 20px;
148
+ font-weight: 600;
149
+ font-size: 13px;
150
+ text-transform: uppercase;
151
+ color: var(--text-muted);
152
+ }
153
+
154
+ tbody tr {
155
+ border-bottom: 1px solid var(--border);
156
+ }
157
+
158
+ tbody tr:hover {
159
+ background: var(--bg-hover);
160
+ }
161
+
162
+ tbody td {
163
+ padding: 16px 20px;
164
+ font-size: 14px;
165
+ }
166
+
167
+ .badge {
168
+ display: inline-block;
169
+ padding: 6px 12px;
170
+ border-radius: 20px;
171
+ font-size: 12px;
172
+ font-weight: 600;
173
+ text-transform: uppercase;
174
+ }
175
+
176
+ .badge.validated {
177
+ background: rgba(16, 185, 129, 0.15);
178
+ color: var(--success);
179
+ }
180
+
181
+ .badge.unvalidated {
182
+ background: rgba(239, 68, 68, 0.15);
183
+ color: var(--danger);
184
+ }
185
+
186
+ .category {
187
+ padding: 4px 10px;
188
+ border-radius: 6px;
189
+ font-size: 12px;
190
+ background: rgba(99, 102, 241, 0.1);
191
+ color: var(--primary);
192
+ }
193
+
194
+ .type {
195
+ padding: 4px 8px;
196
+ border-radius: 4px;
197
+ font-size: 11px;
198
+ background: rgba(59, 130, 246, 0.1);
199
+ color: var(--info);
200
+ }
201
+
202
+ .response-time {
203
+ font-weight: 600;
204
+ }
205
+
206
+ .response-time.fast { color: var(--success); }
207
+ .response-time.medium { color: var(--warning); }
208
+ .response-time.slow { color: var(--danger); }
209
+
210
+ @media (max-width: 768px) {
211
+ .stats-grid {
212
+ grid-template-columns: 1fr;
213
+ }
214
+ .filters {
215
+ flex-direction: column;
216
+ }
217
+ }
218
+ </style>
219
+ </head>
220
+ <body>
221
+ <div class="container">
222
+ <header>
223
+ <h1>
224
+ <span style="font-size: 40px;">📊</span>
225
+ Crypto Provider Monitor
226
+ </h1>
227
+ <p style="color: rgba(255, 255, 255, 0.85);">Real-time API Provider Monitoring Dashboard</p>
228
+ </header>
229
+
230
+ <div class="stats-grid">
231
+ <div class="stat-card">
232
+ <div class="label">Total Providers</div>
233
+ <div class="value" style="color: var(--primary);" id="totalProviders">-</div>
234
+ </div>
235
+ <div class="stat-card">
236
+ <div class="label">✅ Validated</div>
237
+ <div class="value" style="color: var(--success);" id="validatedCount">-</div>
238
+ </div>
239
+ <div class="stat-card">
240
+ <div class="label">❌ Unvalidated</div>
241
+ <div class="value" style="color: var(--danger);" id="unvalidatedCount">-</div>
242
+ </div>
243
+ <div class="stat-card">
244
+ <div class="label">⚡ Avg Response</div>
245
+ <div class="value" style="color: var(--warning); font-size: 28px;" id="avgResponse">- ms</div>
246
+ </div>
247
+ </div>
248
+
249
+ <div class="filters">
250
+ <select id="categoryFilter" onchange="applyFilters()">
251
+ <option value="all">All Categories</option>
252
+ </select>
253
+ <select id="statusFilter" onchange="applyFilters()">
254
+ <option value="all">All Status</option>
255
+ <option value="validated">Validated</option>
256
+ <option value="unvalidated">Unvalidated</option>
257
+ </select>
258
+ <div class="search-box" style="flex: 1; min-width: 250px;">
259
+ <input type="text" id="searchInput" placeholder="Search providers..." onkeyup="applyFilters()">
260
+ </div>
261
+ <button onclick="refreshData()">🔄 Refresh</button>
262
+ </div>
263
+
264
+ <div class="table-container">
265
+ <table>
266
+ <thead>
267
+ <tr>
268
+ <th>Provider ID</th>
269
+ <th>Name</th>
270
+ <th>Category</th>
271
+ <th>Type</th>
272
+ <th>Status</th>
273
+ <th>Response Time</th>
274
+ </tr>
275
+ </thead>
276
+ <tbody id="providersTable">
277
+ <tr><td colspan="6" style="text-align: center; padding: 40px;">Loading...</td></tr>
278
+ </tbody>
279
+ </table>
280
+ </div>
281
+ </div>
282
+
283
+ <script>
284
+ let allProviders = [];
285
+
286
+ // Auto-detect API endpoint
287
+ function getAPIEndpoint() {
288
+ const host = window.location.hostname;
289
+ const port = window.location.port;
290
+ const protocol = window.location.protocol;
291
+
292
+ // For Hugging Face Spaces
293
+ if (host.includes('hf.space') || host.includes('huggingface')) {
294
+ return `${protocol}//${host}/api/providers`;
295
+ }
296
+
297
+ // For local development
298
+ if (port) {
299
+ return `${protocol}//${host}:${port}/api/providers`;
300
+ }
301
+
302
+ return '/api/providers';
303
+ }
304
+
305
+ async function fetchProviders() {
306
+ try {
307
+ const response = await fetch(getAPIEndpoint());
308
+ const data = await response.json();
309
+ allProviders = data.providers || [];
310
+ updateUI();
311
+ } catch (error) {
312
+ console.error('Error:', error);
313
+ document.getElementById('providersTable').innerHTML =
314
+ '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--danger);">Error loading providers</td></tr>';
315
+ }
316
+ }
317
+
318
+ function updateUI() {
319
+ updateStats();
320
+ populateFilters();
321
+ applyFilters();
322
+ }
323
+
324
+ function updateStats() {
325
+ const total = allProviders.length;
326
+ const validated = allProviders.filter(p => p.status === 'validated').length;
327
+ const unvalidated = total - validated;
328
+
329
+ const responseTimes = allProviders
330
+ .filter(p => p.response_time_ms > 0)
331
+ .map(p => p.response_time_ms);
332
+ const avgResponse = responseTimes.length > 0
333
+ ? Math.round(responseTimes.reduce((a, b) => a + b) / responseTimes.length)
334
+ : 0;
335
+
336
+ document.getElementById('totalProviders').textContent = total;
337
+ document.getElementById('validatedCount').textContent = validated;
338
+ document.getElementById('unvalidatedCount').textContent = unvalidated;
339
+ document.getElementById('avgResponse').textContent = avgResponse > 0 ? `${avgResponse} ms` : 'N/A';
340
+ }
341
+
342
+ function populateFilters() {
343
+ const categories = [...new Set(allProviders.map(p => p.category))].filter(c => c).sort();
344
+ const categoryFilter = document.getElementById('categoryFilter');
345
+ categoryFilter.innerHTML = '<option value="all">All Categories</option>';
346
+ categories.forEach(cat => {
347
+ categoryFilter.innerHTML += `<option value="${cat}">${cat.replace(/_/g, ' ').toUpperCase()}</option>`;
348
+ });
349
+ }
350
+
351
+ function applyFilters() {
352
+ const category = document.getElementById('categoryFilter').value;
353
+ const status = document.getElementById('statusFilter').value;
354
+ const search = document.getElementById('searchInput').value.toLowerCase();
355
+
356
+ let filtered = allProviders.filter(p => {
357
+ const matchCategory = category === 'all' || p.category === category;
358
+ const matchStatus = status === 'all' || p.status === status;
359
+ const matchSearch = !search ||
360
+ p.name.toLowerCase().includes(search) ||
361
+ p.provider_id.toLowerCase().includes(search) ||
362
+ (p.category && p.category.toLowerCase().includes(search));
363
+ return matchCategory && matchStatus && matchSearch;
364
+ });
365
+
366
+ renderTable(filtered);
367
+ }
368
+
369
+ function renderTable(providers) {
370
+ const tbody = document.getElementById('providersTable');
371
+
372
+ if (providers.length === 0) {
373
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px;">No providers found</td></tr>';
374
+ return;
375
+ }
376
+
377
+ tbody.innerHTML = providers.map(p => {
378
+ const responseTime = p.response_time_ms || 0;
379
+ let responseClass = 'fast';
380
+ if (responseTime > 500) responseClass = 'slow';
381
+ else if (responseTime > 200) responseClass = 'medium';
382
+
383
+ return `
384
+ <tr>
385
+ <td><code style="color: var(--text-muted); font-size: 12px;">${p.provider_id}</code></td>
386
+ <td><strong>${p.name}</strong></td>
387
+ <td><span class="category">${(p.category || 'unknown').replace(/_/g, ' ')}</span></td>
388
+ <td><span class="type">${(p.type || 'unknown').replace(/_/g, ' ')}</span></td>
389
+ <td><span class="badge ${p.status}">${p.status}</span></td>
390
+ <td>${responseTime > 0 ?
391
+ `<span class="response-time ${responseClass}">${Math.round(responseTime)} ms</span>` :
392
+ '<span style="color: var(--text-muted);">N/A</span>'
393
+ }</td>
394
+ </tr>
395
+ `;
396
+ }).join('');
397
+ }
398
+
399
+ function refreshData() {
400
+ fetchProviders();
401
+ }
402
+
403
+ // Initial load
404
+ fetchProviders();
405
+
406
+ // Auto-refresh every 30 seconds
407
+ setInterval(fetchProviders, 30000);
408
+ </script>
409
+ </body>
410
+ </html>
archive_html/enhanced_dashboard.html ADDED
@@ -0,0 +1,876 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Enhanced Crypto Data Tracker</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1600px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ header {
27
+ background: rgba(255, 255, 255, 0.1);
28
+ backdrop-filter: blur(10px);
29
+ border-radius: 15px;
30
+ padding: 20px 30px;
31
+ margin-bottom: 20px;
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ }
36
+
37
+ h1 {
38
+ color: white;
39
+ font-size: 28px;
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 10px;
43
+ }
44
+
45
+ .connection-status {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ color: white;
50
+ font-size: 14px;
51
+ }
52
+
53
+ .status-indicator {
54
+ width: 10px;
55
+ height: 10px;
56
+ border-radius: 50%;
57
+ background: #10b981;
58
+ animation: pulse 2s infinite;
59
+ }
60
+
61
+ .status-indicator.disconnected {
62
+ background: #ef4444;
63
+ animation: none;
64
+ }
65
+
66
+ @keyframes pulse {
67
+ 0%, 100% { opacity: 1; }
68
+ 50% { opacity: 0.5; }
69
+ }
70
+
71
+ .controls {
72
+ background: rgba(255, 255, 255, 0.1);
73
+ backdrop-filter: blur(10px);
74
+ border-radius: 15px;
75
+ padding: 20px;
76
+ margin-bottom: 20px;
77
+ }
78
+
79
+ .controls-row {
80
+ display: flex;
81
+ gap: 15px;
82
+ flex-wrap: wrap;
83
+ align-items: center;
84
+ }
85
+
86
+ .btn {
87
+ padding: 10px 20px;
88
+ border: none;
89
+ border-radius: 8px;
90
+ cursor: pointer;
91
+ font-size: 14px;
92
+ font-weight: 600;
93
+ transition: all 0.3s ease;
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 8px;
97
+ }
98
+
99
+ .btn-primary {
100
+ background: white;
101
+ color: #667eea;
102
+ }
103
+
104
+ .btn-primary:hover {
105
+ transform: translateY(-2px);
106
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
107
+ }
108
+
109
+ .btn-success {
110
+ background: #10b981;
111
+ color: white;
112
+ }
113
+
114
+ .btn-danger {
115
+ background: #ef4444;
116
+ color: white;
117
+ }
118
+
119
+ .btn-info {
120
+ background: #3b82f6;
121
+ color: white;
122
+ }
123
+
124
+ .btn:disabled {
125
+ opacity: 0.5;
126
+ cursor: not-allowed;
127
+ }
128
+
129
+ .grid {
130
+ display: grid;
131
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
132
+ gap: 20px;
133
+ margin-bottom: 20px;
134
+ }
135
+
136
+ .card {
137
+ background: rgba(255, 255, 255, 0.1);
138
+ backdrop-filter: blur(10px);
139
+ border-radius: 15px;
140
+ padding: 20px;
141
+ color: white;
142
+ }
143
+
144
+ .card h2 {
145
+ font-size: 18px;
146
+ margin-bottom: 15px;
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 10px;
150
+ }
151
+
152
+ .stat-grid {
153
+ display: grid;
154
+ grid-template-columns: repeat(2, 1fr);
155
+ gap: 15px;
156
+ }
157
+
158
+ .stat-item {
159
+ background: rgba(255, 255, 255, 0.1);
160
+ padding: 15px;
161
+ border-radius: 10px;
162
+ }
163
+
164
+ .stat-label {
165
+ font-size: 12px;
166
+ opacity: 0.8;
167
+ margin-bottom: 5px;
168
+ }
169
+
170
+ .stat-value {
171
+ font-size: 24px;
172
+ font-weight: 700;
173
+ }
174
+
175
+ .api-list {
176
+ max-height: 400px;
177
+ overflow-y: auto;
178
+ }
179
+
180
+ .api-item {
181
+ background: rgba(255, 255, 255, 0.05);
182
+ padding: 15px;
183
+ border-radius: 10px;
184
+ margin-bottom: 10px;
185
+ display: flex;
186
+ justify-content: space-between;
187
+ align-items: center;
188
+ }
189
+
190
+ .api-info {
191
+ flex: 1;
192
+ }
193
+
194
+ .api-name {
195
+ font-weight: 600;
196
+ margin-bottom: 5px;
197
+ }
198
+
199
+ .api-meta {
200
+ font-size: 12px;
201
+ opacity: 0.7;
202
+ display: flex;
203
+ gap: 15px;
204
+ }
205
+
206
+ .api-controls {
207
+ display: flex;
208
+ gap: 10px;
209
+ }
210
+
211
+ .small-btn {
212
+ padding: 5px 12px;
213
+ font-size: 12px;
214
+ border-radius: 5px;
215
+ border: none;
216
+ cursor: pointer;
217
+ background: rgba(255, 255, 255, 0.2);
218
+ color: white;
219
+ transition: all 0.2s;
220
+ }
221
+
222
+ .small-btn:hover {
223
+ background: rgba(255, 255, 255, 0.3);
224
+ }
225
+
226
+ .status-badge {
227
+ display: inline-block;
228
+ padding: 4px 10px;
229
+ border-radius: 12px;
230
+ font-size: 11px;
231
+ font-weight: 600;
232
+ }
233
+
234
+ .status-success {
235
+ background: #10b981;
236
+ color: white;
237
+ }
238
+
239
+ .status-pending {
240
+ background: #f59e0b;
241
+ color: white;
242
+ }
243
+
244
+ .status-failed {
245
+ background: #ef4444;
246
+ color: white;
247
+ }
248
+
249
+ .log-container {
250
+ background: rgba(0, 0, 0, 0.3);
251
+ border-radius: 10px;
252
+ padding: 15px;
253
+ max-height: 300px;
254
+ overflow-y: auto;
255
+ font-family: 'Courier New', monospace;
256
+ font-size: 12px;
257
+ }
258
+
259
+ .log-entry {
260
+ margin-bottom: 8px;
261
+ padding: 5px;
262
+ border-left: 3px solid #667eea;
263
+ padding-left: 10px;
264
+ }
265
+
266
+ .log-time {
267
+ opacity: 0.6;
268
+ margin-right: 10px;
269
+ }
270
+
271
+ .modal {
272
+ display: none;
273
+ position: fixed;
274
+ top: 0;
275
+ left: 0;
276
+ width: 100%;
277
+ height: 100%;
278
+ background: rgba(0, 0, 0, 0.7);
279
+ z-index: 1000;
280
+ justify-content: center;
281
+ align-items: center;
282
+ }
283
+
284
+ .modal.active {
285
+ display: flex;
286
+ }
287
+
288
+ .modal-content {
289
+ background: white;
290
+ border-radius: 15px;
291
+ padding: 30px;
292
+ max-width: 500px;
293
+ width: 90%;
294
+ color: #333;
295
+ }
296
+
297
+ .modal-header {
298
+ display: flex;
299
+ justify-content: space-between;
300
+ align-items: center;
301
+ margin-bottom: 20px;
302
+ }
303
+
304
+ .modal-close {
305
+ background: none;
306
+ border: none;
307
+ font-size: 24px;
308
+ cursor: pointer;
309
+ color: #666;
310
+ }
311
+
312
+ .form-group {
313
+ margin-bottom: 15px;
314
+ }
315
+
316
+ .form-label {
317
+ display: block;
318
+ margin-bottom: 5px;
319
+ font-weight: 600;
320
+ color: #333;
321
+ }
322
+
323
+ .form-input {
324
+ width: 100%;
325
+ padding: 10px;
326
+ border: 1px solid #ddd;
327
+ border-radius: 8px;
328
+ font-size: 14px;
329
+ }
330
+
331
+ .form-select {
332
+ width: 100%;
333
+ padding: 10px;
334
+ border: 1px solid #ddd;
335
+ border-radius: 8px;
336
+ font-size: 14px;
337
+ }
338
+
339
+ ::-webkit-scrollbar {
340
+ width: 8px;
341
+ }
342
+
343
+ ::-webkit-scrollbar-track {
344
+ background: rgba(255, 255, 255, 0.1);
345
+ border-radius: 4px;
346
+ }
347
+
348
+ ::-webkit-scrollbar-thumb {
349
+ background: rgba(255, 255, 255, 0.3);
350
+ border-radius: 4px;
351
+ }
352
+
353
+ ::-webkit-scrollbar-thumb:hover {
354
+ background: rgba(255, 255, 255, 0.5);
355
+ }
356
+
357
+ .toast {
358
+ position: fixed;
359
+ bottom: 20px;
360
+ right: 20px;
361
+ background: white;
362
+ color: #333;
363
+ padding: 15px 20px;
364
+ border-radius: 10px;
365
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
366
+ opacity: 0;
367
+ transform: translateY(20px);
368
+ transition: all 0.3s ease;
369
+ z-index: 2000;
370
+ }
371
+
372
+ .toast.show {
373
+ opacity: 1;
374
+ transform: translateY(0);
375
+ }
376
+ </style>
377
+ </head>
378
+ <body>
379
+ <div class="container">
380
+ <header>
381
+ <h1>
382
+ <span>🚀</span>
383
+ Enhanced Crypto Data Tracker
384
+ </h1>
385
+ <div class="connection-status">
386
+ <div class="status-indicator" id="wsStatus"></div>
387
+ <span id="wsStatusText">Connecting...</span>
388
+ </div>
389
+ </header>
390
+
391
+ <div class="controls">
392
+ <div class="controls-row">
393
+ <button class="btn btn-primary" onclick="exportJSON()">
394
+ 💾 Export JSON
395
+ </button>
396
+ <button class="btn btn-primary" onclick="exportCSV()">
397
+ 📊 Export CSV
398
+ </button>
399
+ <button class="btn btn-success" onclick="createBackup()">
400
+ 🔄 Create Backup
401
+ </button>
402
+ <button class="btn btn-info" onclick="showScheduleModal()">
403
+ ⏰ Configure Schedule
404
+ </button>
405
+ <button class="btn btn-info" onclick="forceUpdateAll()">
406
+ 🔃 Force Update All
407
+ </button>
408
+ <button class="btn btn-danger" onclick="clearCache()">
409
+ 🗑️ Clear Cache
410
+ </button>
411
+ </div>
412
+ </div>
413
+
414
+ <div class="grid">
415
+ <div class="card">
416
+ <h2>📊 System Statistics</h2>
417
+ <div class="stat-grid">
418
+ <div class="stat-item">
419
+ <div class="stat-label">Total APIs</div>
420
+ <div class="stat-value" id="totalApis">0</div>
421
+ </div>
422
+ <div class="stat-item">
423
+ <div class="stat-label">Active Tasks</div>
424
+ <div class="stat-value" id="activeTasks">0</div>
425
+ </div>
426
+ <div class="stat-item">
427
+ <div class="stat-label">Cached Data</div>
428
+ <div class="stat-value" id="cachedData">0</div>
429
+ </div>
430
+ <div class="stat-item">
431
+ <div class="stat-label">WS Connections</div>
432
+ <div class="stat-value" id="wsConnections">0</div>
433
+ </div>
434
+ </div>
435
+ </div>
436
+
437
+ <div class="card">
438
+ <h2>📈 Recent Activity</h2>
439
+ <div class="log-container" id="activityLog">
440
+ <div class="log-entry">
441
+ <span class="log-time">--:--:--</span>
442
+ Waiting for updates...
443
+ </div>
444
+ </div>
445
+ </div>
446
+ </div>
447
+
448
+ <div class="card">
449
+ <h2>🔌 API Sources</h2>
450
+ <div class="api-list" id="apiList">
451
+ Loading...
452
+ </div>
453
+ </div>
454
+ </div>
455
+
456
+ <!-- Schedule Modal -->
457
+ <div class="modal" id="scheduleModal">
458
+ <div class="modal-content">
459
+ <div class="modal-header">
460
+ <h2>⏰ Configure Schedule</h2>
461
+ <button class="modal-close" onclick="closeScheduleModal()">×</button>
462
+ </div>
463
+ <div class="form-group">
464
+ <label class="form-label">API Source</label>
465
+ <select class="form-select" id="scheduleApiSelect"></select>
466
+ </div>
467
+ <div class="form-group">
468
+ <label class="form-label">Interval (seconds)</label>
469
+ <input type="number" class="form-input" id="scheduleInterval" value="60" min="10">
470
+ </div>
471
+ <div class="form-group">
472
+ <label class="form-label">Enabled</label>
473
+ <input type="checkbox" id="scheduleEnabled" checked>
474
+ </div>
475
+ <button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button>
476
+ </div>
477
+ </div>
478
+
479
+ <!-- Toast notification -->
480
+ <div class="toast" id="toast"></div>
481
+
482
+ <script>
483
+ let ws = null;
484
+ let reconnectAttempts = 0;
485
+ const maxReconnectAttempts = 5;
486
+ const reconnectDelay = 3000;
487
+
488
+ // Initialize WebSocket connection
489
+ function initWebSocket() {
490
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
491
+ const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`;
492
+
493
+ ws = new WebSocket(wsUrl);
494
+
495
+ ws.onopen = () => {
496
+ console.log('WebSocket connected');
497
+ updateWSStatus(true);
498
+ reconnectAttempts = 0;
499
+
500
+ // Subscribe to all updates
501
+ ws.send(JSON.stringify({ type: 'subscribe_all' }));
502
+
503
+ // Start heartbeat
504
+ startHeartbeat();
505
+ };
506
+
507
+ ws.onmessage = (event) => {
508
+ const message = JSON.parse(event.data);
509
+ handleWSMessage(message);
510
+ };
511
+
512
+ ws.onerror = (error) => {
513
+ console.error('WebSocket error:', error);
514
+ };
515
+
516
+ ws.onclose = () => {
517
+ console.log('WebSocket disconnected');
518
+ updateWSStatus(false);
519
+ attemptReconnect();
520
+ };
521
+ }
522
+
523
+ function attemptReconnect() {
524
+ if (reconnectAttempts < maxReconnectAttempts) {
525
+ reconnectAttempts++;
526
+ console.log(`Reconnecting... Attempt ${reconnectAttempts}`);
527
+ setTimeout(initWebSocket, reconnectDelay);
528
+ }
529
+ }
530
+
531
+ let heartbeatInterval;
532
+ function startHeartbeat() {
533
+ clearInterval(heartbeatInterval);
534
+ heartbeatInterval = setInterval(() => {
535
+ if (ws && ws.readyState === WebSocket.OPEN) {
536
+ ws.send(JSON.stringify({ type: 'ping' }));
537
+ }
538
+ }, 30000);
539
+ }
540
+
541
+ function updateWSStatus(connected) {
542
+ const indicator = document.getElementById('wsStatus');
543
+ const text = document.getElementById('wsStatusText');
544
+
545
+ if (connected) {
546
+ indicator.classList.remove('disconnected');
547
+ text.textContent = 'Connected';
548
+ } else {
549
+ indicator.classList.add('disconnected');
550
+ text.textContent = 'Disconnected';
551
+ }
552
+ }
553
+
554
+ function handleWSMessage(message) {
555
+ console.log('Received:', message);
556
+
557
+ switch (message.type) {
558
+ case 'api_update':
559
+ handleApiUpdate(message);
560
+ break;
561
+ case 'status_update':
562
+ handleStatusUpdate(message);
563
+ break;
564
+ case 'schedule_update':
565
+ handleScheduleUpdate(message);
566
+ break;
567
+ case 'subscribed':
568
+ addLog(`Subscribed to ${message.api_id || 'all updates'}`);
569
+ break;
570
+ }
571
+ }
572
+
573
+ function handleApiUpdate(message) {
574
+ addLog(`Updated: ${message.api_id}`, 'success');
575
+ loadSystemStatus();
576
+ }
577
+
578
+ function handleStatusUpdate(message) {
579
+ addLog('System status updated');
580
+ loadSystemStatus();
581
+ }
582
+
583
+ function handleScheduleUpdate(message) {
584
+ addLog(`Schedule updated for ${message.schedule.api_id}`);
585
+ loadAPIs();
586
+ }
587
+
588
+ function addLog(text, type = 'info') {
589
+ const logContainer = document.getElementById('activityLog');
590
+ const time = new Date().toLocaleTimeString();
591
+
592
+ const entry = document.createElement('div');
593
+ entry.className = 'log-entry';
594
+ entry.innerHTML = `<span class="log-time">${time}</span>${text}`;
595
+
596
+ logContainer.insertBefore(entry, logContainer.firstChild);
597
+
598
+ // Keep only last 50 entries
599
+ while (logContainer.children.length > 50) {
600
+ logContainer.removeChild(logContainer.lastChild);
601
+ }
602
+ }
603
+
604
+ function showToast(message, duration = 3000) {
605
+ const toast = document.getElementById('toast');
606
+ toast.textContent = message;
607
+ toast.classList.add('show');
608
+
609
+ setTimeout(() => {
610
+ toast.classList.remove('show');
611
+ }, duration);
612
+ }
613
+
614
+ // Load system status
615
+ async function loadSystemStatus() {
616
+ try {
617
+ const response = await fetch('/api/v2/status');
618
+ const data = await response.json();
619
+
620
+ document.getElementById('totalApis').textContent =
621
+ data.services.config_loader.apis_loaded;
622
+ document.getElementById('activeTasks').textContent =
623
+ data.services.scheduler.total_tasks;
624
+ document.getElementById('cachedData').textContent =
625
+ data.services.persistence.cached_apis;
626
+ document.getElementById('wsConnections').textContent =
627
+ data.services.websocket.total_connections;
628
+
629
+ } catch (error) {
630
+ console.error('Error loading status:', error);
631
+ }
632
+ }
633
+
634
+ // Load APIs
635
+ async function loadAPIs() {
636
+ try {
637
+ const response = await fetch('/api/v2/config/apis');
638
+ const data = await response.json();
639
+
640
+ const scheduleResponse = await fetch('/api/v2/schedule/tasks');
641
+ const schedules = await scheduleResponse.json();
642
+
643
+ displayAPIs(data.apis, schedules);
644
+
645
+ } catch (error) {
646
+ console.error('Error loading APIs:', error);
647
+ }
648
+ }
649
+
650
+ function displayAPIs(apis, schedules) {
651
+ const listElement = document.getElementById('apiList');
652
+ listElement.innerHTML = '';
653
+
654
+ for (const [apiId, api] of Object.entries(apis)) {
655
+ const schedule = schedules[apiId] || {};
656
+
657
+ const item = document.createElement('div');
658
+ item.className = 'api-item';
659
+ item.innerHTML = `
660
+ <div class="api-info">
661
+ <div class="api-name">${api.name}</div>
662
+ <div class="api-meta">
663
+ <span>📂 ${api.category}</span>
664
+ <span>⏱️ ${schedule.interval || 300}s</span>
665
+ <span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}">
666
+ ${schedule.last_status || 'pending'}
667
+ </span>
668
+ </div>
669
+ </div>
670
+ <div class="api-controls">
671
+ <button class="small-btn" onclick="forceUpdate('${apiId}')">🔄 Update</button>
672
+ <button class="small-btn" onclick="showScheduleModalFor('${apiId}')">⚙️ Schedule</button>
673
+ </div>
674
+ `;
675
+
676
+ listElement.appendChild(item);
677
+ }
678
+ }
679
+
680
+ // Export functions
681
+ async function exportJSON() {
682
+ try {
683
+ const response = await fetch('/api/v2/export/json', {
684
+ method: 'POST',
685
+ headers: { 'Content-Type': 'application/json' },
686
+ body: JSON.stringify({ include_history: true })
687
+ });
688
+
689
+ const data = await response.json();
690
+ showToast('✅ JSON export created!');
691
+ addLog(`Exported to JSON: ${data.filepath}`);
692
+
693
+ // Trigger download
694
+ window.open(data.download_url, '_blank');
695
+
696
+ } catch (error) {
697
+ showToast('❌ Export failed');
698
+ console.error(error);
699
+ }
700
+ }
701
+
702
+ async function exportCSV() {
703
+ try {
704
+ const response = await fetch('/api/v2/export/csv', {
705
+ method: 'POST',
706
+ headers: { 'Content-Type': 'application/json' },
707
+ body: JSON.stringify({ flatten: true })
708
+ });
709
+
710
+ const data = await response.json();
711
+ showToast('✅ CSV export created!');
712
+ addLog(`Exported to CSV: ${data.filepath}`);
713
+
714
+ // Trigger download
715
+ window.open(data.download_url, '_blank');
716
+
717
+ } catch (error) {
718
+ showToast('❌ Export failed');
719
+ console.error(error);
720
+ }
721
+ }
722
+
723
+ async function createBackup() {
724
+ try {
725
+ const response = await fetch('/api/v2/backup', {
726
+ method: 'POST'
727
+ });
728
+
729
+ const data = await response.json();
730
+ showToast('✅ Backup created!');
731
+ addLog(`Backup created: ${data.backup_file}`);
732
+
733
+ } catch (error) {
734
+ showToast('❌ Backup failed');
735
+ console.error(error);
736
+ }
737
+ }
738
+
739
+ async function forceUpdate(apiId) {
740
+ try {
741
+ const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, {
742
+ method: 'POST'
743
+ });
744
+
745
+ const data = await response.json();
746
+ showToast(`✅ ${apiId} updated!`);
747
+ addLog(`Forced update: ${apiId}`);
748
+ loadAPIs();
749
+
750
+ } catch (error) {
751
+ showToast('❌ Update failed');
752
+ console.error(error);
753
+ }
754
+ }
755
+
756
+ async function forceUpdateAll() {
757
+ showToast('🔄 Updating all APIs...');
758
+ addLog('Forcing update for all APIs');
759
+
760
+ try {
761
+ const response = await fetch('/api/v2/config/apis');
762
+ const data = await response.json();
763
+
764
+ for (const apiId of Object.keys(data.apis)) {
765
+ await forceUpdate(apiId);
766
+ await new Promise(resolve => setTimeout(resolve, 100)); // Small delay
767
+ }
768
+
769
+ showToast('✅ All APIs updated!');
770
+ } catch (error) {
771
+ showToast('❌ Update failed');
772
+ console.error(error);
773
+ }
774
+ }
775
+
776
+ async function clearCache() {
777
+ if (!confirm('Clear all cached data?')) return;
778
+
779
+ try {
780
+ const response = await fetch('/api/v2/cleanup/cache', {
781
+ method: 'POST'
782
+ });
783
+
784
+ showToast('✅ Cache cleared!');
785
+ addLog('Cache cleared');
786
+ loadSystemStatus();
787
+
788
+ } catch (error) {
789
+ showToast('❌ Failed to clear cache');
790
+ console.error(error);
791
+ }
792
+ }
793
+
794
+ // Schedule modal functions
795
+ function showScheduleModal() {
796
+ loadAPISelectOptions();
797
+ document.getElementById('scheduleModal').classList.add('active');
798
+ }
799
+
800
+ function closeScheduleModal() {
801
+ document.getElementById('scheduleModal').classList.remove('active');
802
+ }
803
+
804
+ async function showScheduleModalFor(apiId) {
805
+ await loadAPISelectOptions();
806
+ document.getElementById('scheduleApiSelect').value = apiId;
807
+
808
+ // Load current schedule
809
+ try {
810
+ const response = await fetch(`/api/v2/schedule/tasks/${apiId}`);
811
+ const schedule = await response.json();
812
+
813
+ document.getElementById('scheduleInterval').value = schedule.interval;
814
+ document.getElementById('scheduleEnabled').checked = schedule.enabled;
815
+
816
+ } catch (error) {
817
+ console.error(error);
818
+ }
819
+
820
+ showScheduleModal();
821
+ }
822
+
823
+ async function loadAPISelectOptions() {
824
+ try {
825
+ const response = await fetch('/api/v2/config/apis');
826
+ const data = await response.json();
827
+
828
+ const select = document.getElementById('scheduleApiSelect');
829
+ select.innerHTML = '';
830
+
831
+ for (const [apiId, api] of Object.entries(data.apis)) {
832
+ const option = document.createElement('option');
833
+ option.value = apiId;
834
+ option.textContent = api.name;
835
+ select.appendChild(option);
836
+ }
837
+
838
+ } catch (error) {
839
+ console.error(error);
840
+ }
841
+ }
842
+
843
+ async function updateSchedule() {
844
+ const apiId = document.getElementById('scheduleApiSelect').value;
845
+ const interval = parseInt(document.getElementById('scheduleInterval').value);
846
+ const enabled = document.getElementById('scheduleEnabled').checked;
847
+
848
+ try {
849
+ const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, {
850
+ method: 'PUT'
851
+ });
852
+
853
+ const data = await response.json();
854
+ showToast('✅ Schedule updated!');
855
+ addLog(`Updated schedule for ${apiId}`);
856
+ closeScheduleModal();
857
+ loadAPIs();
858
+
859
+ } catch (error) {
860
+ showToast('❌ Schedule update failed');
861
+ console.error(error);
862
+ }
863
+ }
864
+
865
+ // Initialize on load
866
+ window.addEventListener('load', () => {
867
+ initWebSocket();
868
+ loadSystemStatus();
869
+ loadAPIs();
870
+
871
+ // Refresh status every 30 seconds
872
+ setInterval(loadSystemStatus, 30000);
873
+ });
874
+ </script>
875
+ </body>
876
+ </html>
archive_html/feature_flags_demo.html ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Monitor - Feature Flags Demo</title>
7
+ <link rel="stylesheet" href="/static/css/mobile-responsive.css">
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
17
+ background: #f5f7fa;
18
+ padding: 20px;
19
+ }
20
+
21
+ .header {
22
+ background: #fff;
23
+ padding: 20px;
24
+ border-radius: 8px;
25
+ margin-bottom: 20px;
26
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
27
+ }
28
+
29
+ h1 {
30
+ color: #333;
31
+ margin-bottom: 10px;
32
+ }
33
+
34
+ .subtitle {
35
+ color: #666;
36
+ font-size: 0.95rem;
37
+ }
38
+
39
+ .dashboard {
40
+ display: grid;
41
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
42
+ gap: 20px;
43
+ }
44
+
45
+ .card {
46
+ background: #fff;
47
+ border-radius: 8px;
48
+ padding: 20px;
49
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
50
+ }
51
+
52
+ .card h3 {
53
+ margin-bottom: 15px;
54
+ color: #333;
55
+ }
56
+
57
+ .stats-grid {
58
+ display: grid;
59
+ grid-template-columns: repeat(2, 1fr);
60
+ gap: 15px;
61
+ margin-top: 15px;
62
+ }
63
+
64
+ .stat-item {
65
+ padding: 15px;
66
+ background: #f8f9fa;
67
+ border-radius: 6px;
68
+ text-align: center;
69
+ }
70
+
71
+ .stat-label {
72
+ font-size: 0.85rem;
73
+ color: #666;
74
+ margin-bottom: 8px;
75
+ }
76
+
77
+ .stat-value {
78
+ font-size: 1.8rem;
79
+ font-weight: 700;
80
+ color: #333;
81
+ }
82
+
83
+ .btn {
84
+ padding: 10px 20px;
85
+ background: #007bff;
86
+ color: #fff;
87
+ border: none;
88
+ border-radius: 4px;
89
+ cursor: pointer;
90
+ font-size: 0.95rem;
91
+ transition: background 0.2s;
92
+ }
93
+
94
+ .btn:hover {
95
+ background: #0056b3;
96
+ }
97
+
98
+ .btn-secondary {
99
+ background: #6c757d;
100
+ }
101
+
102
+ .btn-secondary:hover {
103
+ background: #5a6268;
104
+ }
105
+
106
+ .provider-list {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 10px;
110
+ margin-top: 15px;
111
+ }
112
+
113
+ .provider-item {
114
+ padding: 12px;
115
+ background: #f8f9fa;
116
+ border-radius: 6px;
117
+ display: flex;
118
+ justify-content: space-between;
119
+ align-items: center;
120
+ border-left: 4px solid #ccc;
121
+ }
122
+
123
+ .provider-item.online {
124
+ border-left-color: #28a745;
125
+ }
126
+
127
+ .provider-item.degraded {
128
+ border-left-color: #ffc107;
129
+ }
130
+
131
+ .provider-item.offline {
132
+ border-left-color: #dc3545;
133
+ }
134
+
135
+ .provider-info {
136
+ flex: 1;
137
+ }
138
+
139
+ .provider-name {
140
+ font-weight: 600;
141
+ color: #333;
142
+ margin-bottom: 4px;
143
+ }
144
+
145
+ .provider-meta {
146
+ font-size: 0.85rem;
147
+ color: #666;
148
+ }
149
+
150
+ .proxy-indicator {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 4px;
154
+ padding: 4px 8px;
155
+ background: #fff3cd;
156
+ color: #856404;
157
+ border-radius: 4px;
158
+ font-size: 0.75rem;
159
+ margin-left: 8px;
160
+ }
161
+
162
+ .footer {
163
+ margin-top: 30px;
164
+ text-align: center;
165
+ color: #666;
166
+ font-size: 0.85rem;
167
+ }
168
+
169
+ @media screen and (max-width: 768px) {
170
+ .dashboard {
171
+ grid-template-columns: 1fr;
172
+ }
173
+
174
+ .stats-grid {
175
+ grid-template-columns: 1fr;
176
+ }
177
+ }
178
+ </style>
179
+ </head>
180
+ <body>
181
+ <div class="header">
182
+ <h1>🚀 Crypto Monitor - Feature Flags Demo</h1>
183
+ <p class="subtitle">Enterprise-Grade API Monitoring with Smart Proxy Mode</p>
184
+ </div>
185
+
186
+ <div class="dashboard">
187
+ <!-- Feature Flags Card -->
188
+ <div class="card">
189
+ <div id="feature-flags-container"></div>
190
+ </div>
191
+
192
+ <!-- System Status Card -->
193
+ <div class="card">
194
+ <h3>📊 System Status</h3>
195
+ <div class="stats-grid">
196
+ <div class="stat-item">
197
+ <div class="stat-label">Total Providers</div>
198
+ <div class="stat-value" id="stat-total">-</div>
199
+ </div>
200
+ <div class="stat-item">
201
+ <div class="stat-label">Online</div>
202
+ <div class="stat-value" id="stat-online" style="color: #28a745;">-</div>
203
+ </div>
204
+ <div class="stat-item">
205
+ <div class="stat-label">Using Proxy</div>
206
+ <div class="stat-value" id="stat-proxy" style="color: #ffc107;">-</div>
207
+ </div>
208
+ <div class="stat-item">
209
+ <div class="stat-label">Avg Response</div>
210
+ <div class="stat-value" id="stat-response">-</div>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <!-- Provider Health Card -->
216
+ <div class="card" style="grid-column: span 2;">
217
+ <h3>🔧 Provider Health Status</h3>
218
+ <div class="provider-list" id="provider-list">
219
+ <p style="color: #666;">Loading providers...</p>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Proxy Status Card -->
224
+ <div class="card">
225
+ <h3>🌐 Smart Proxy Status</h3>
226
+ <div id="proxy-status">
227
+ <p style="color: #666;">Loading proxy status...</p>
228
+ </div>
229
+ </div>
230
+ </div>
231
+
232
+ <div class="footer">
233
+ <p>Crypto Monitor ULTIMATE - Enterprise Edition &copy; 2025</p>
234
+ <p>Powered by Real APIs • Smart Proxy Mode • Feature Flags</p>
235
+ </div>
236
+
237
+ <!-- Mobile Navigation (shows on mobile only) -->
238
+ <div class="mobile-nav-bottom">
239
+ <div class="nav-items">
240
+ <div class="nav-item">
241
+ <a href="#" class="nav-link active">
242
+ <span class="nav-icon">📊</span>
243
+ <span>Dashboard</span>
244
+ </a>
245
+ </div>
246
+ <div class="nav-item">
247
+ <a href="#" class="nav-link">
248
+ <span class="nav-icon">🔧</span>
249
+ <span>Providers</span>
250
+ </a>
251
+ </div>
252
+ <div class="nav-item">
253
+ <a href="#" class="nav-link">
254
+ <span class="nav-icon">⚙️</span>
255
+ <span>Settings</span>
256
+ </a>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <script src="/static/js/feature-flags.js"></script>
262
+ <script>
263
+ // Initialize Feature Flags UI
264
+ document.addEventListener('DOMContentLoaded', async () => {
265
+ await window.featureFlagsManager.init();
266
+ window.featureFlagsManager.renderUI('feature-flags-container');
267
+
268
+ // Load system status
269
+ loadSystemStatus();
270
+
271
+ // Load provider health
272
+ loadProviderHealth();
273
+
274
+ // Load proxy status
275
+ loadProxyStatus();
276
+
277
+ // Auto-refresh every 30 seconds
278
+ setInterval(() => {
279
+ loadSystemStatus();
280
+ loadProviderHealth();
281
+ loadProxyStatus();
282
+ }, 30000);
283
+ });
284
+
285
+ async function loadSystemStatus() {
286
+ try {
287
+ const response = await fetch('/api/status');
288
+ const data = await response.json();
289
+
290
+ document.getElementById('stat-total').textContent = data.total_providers || 0;
291
+ document.getElementById('stat-online').textContent = data.online || 0;
292
+ document.getElementById('stat-response').textContent =
293
+ data.avg_response_time_ms ? `${Math.round(data.avg_response_time_ms)}ms` : '-';
294
+
295
+ // Get proxy status
296
+ const proxyResp = await fetch('/api/proxy-status');
297
+ const proxyData = await proxyResp.json();
298
+ document.getElementById('stat-proxy').textContent =
299
+ proxyData.total_providers_using_proxy || 0;
300
+ } catch (error) {
301
+ console.error('Error loading system status:', error);
302
+ }
303
+ }
304
+
305
+ async function loadProviderHealth() {
306
+ try {
307
+ const response = await fetch('/api/providers');
308
+ const providers = await response.json();
309
+
310
+ if (!Array.isArray(providers)) {
311
+ return;
312
+ }
313
+
314
+ const listEl = document.getElementById('provider-list');
315
+ let html = '';
316
+
317
+ providers.slice(0, 10).forEach(provider => {
318
+ const status = provider.status || 'unknown';
319
+ const statusClass = status.toLowerCase();
320
+
321
+ html += `
322
+ <div class="provider-item ${statusClass}">
323
+ <div class="provider-info">
324
+ <div class="provider-name">${provider.name}</div>
325
+ <div class="provider-meta">
326
+ ${provider.category || 'unknown'} •
327
+ ${provider.response_time_ms ? provider.response_time_ms + 'ms' : 'N/A'}
328
+ </div>
329
+ </div>
330
+ <div>
331
+ <span class="provider-status-badge ${statusClass}">
332
+ ${status === 'online' ? '✓' : status === 'degraded' ? '⚠' : '✗'}
333
+ ${status.toUpperCase()}
334
+ </span>
335
+ </div>
336
+ </div>
337
+ `;
338
+ });
339
+
340
+ listEl.innerHTML = html || '<p style="color: #666;">No providers found</p>';
341
+ } catch (error) {
342
+ console.error('Error loading provider health:', error);
343
+ }
344
+ }
345
+
346
+ async function loadProxyStatus() {
347
+ try {
348
+ const response = await fetch('/api/proxy-status');
349
+ const data = await response.json();
350
+
351
+ const statusEl = document.getElementById('proxy-status');
352
+
353
+ let html = `
354
+ <div style="margin-bottom: 15px;">
355
+ <strong>Auto Mode:</strong>
356
+ <span style="color: ${data.proxy_auto_mode_enabled ? '#28a745' : '#dc3545'}">
357
+ ${data.proxy_auto_mode_enabled ? '✓ Enabled' : '✗ Disabled'}
358
+ </span>
359
+ </div>
360
+ <div style="margin-bottom: 15px;">
361
+ <strong>Providers Using Proxy:</strong> ${data.total_providers_using_proxy}
362
+ </div>
363
+ `;
364
+
365
+ if (data.providers && data.providers.length > 0) {
366
+ html += '<div style="margin-top: 15px;"><strong>Currently Proxied:</strong></div>';
367
+ html += '<div class="provider-list">';
368
+ data.providers.forEach(p => {
369
+ html += `
370
+ <div class="provider-item">
371
+ <div class="provider-info">
372
+ <div class="provider-name">${p.provider}</div>
373
+ <div class="provider-meta">
374
+ ${p.reason} • Cached ${p.cache_age_seconds}s ago
375
+ </div>
376
+ </div>
377
+ <div class="proxy-indicator">🌐 PROXY</div>
378
+ </div>
379
+ `;
380
+ });
381
+ html += '</div>';
382
+ } else {
383
+ html += '<p style="color: #666; margin-top: 15px;">No providers currently using proxy</p>';
384
+ }
385
+
386
+ statusEl.innerHTML = html;
387
+ } catch (error) {
388
+ console.error('Error loading proxy status:', error);
389
+ }
390
+ }
391
+ </script>
392
+ </body>
393
+ </html>
archive_html/hf_console.html ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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" />
6
+ <title>HF Console · Crypto Intelligence</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
+ <link rel="stylesheet" href="/static/css/unified-ui.css" />
11
+ <link rel="stylesheet" href="/static/css/components.css" />
12
+ <script defer src="/static/js/ui-feedback.js"></script>
13
+ <script defer src="/static/js/hf-console.js"></script>
14
+ </head>
15
+ <body class="page page-hf-console">
16
+ <header class="top-nav">
17
+ <div class="branding">
18
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
19
+ <div>
20
+ <strong>HF Models & Datasets</strong>
21
+ <small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/hf/* endpoints</small>
22
+ </div>
23
+ </div>
24
+ <nav class="nav-links">
25
+ <a href="/dashboard">Dashboard</a>
26
+ <a href="/admin">Admin</a>
27
+ <a class="active" href="/hf_console">HF Console</a>
28
+ <a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
29
+ </nav>
30
+ </header>
31
+
32
+ <main class="page-content">
33
+ <section class="card">
34
+ <div class="section-heading">
35
+ <h2>Registry & Status</h2>
36
+ <span class="badge info" id="hf-console-health">Loading...</span>
37
+ </div>
38
+ <p class="metric-subtext" id="hf-console-summary"></p>
39
+ <ul class="list" id="hf-console-models"></ul>
40
+ </section>
41
+
42
+ <section class="split-grid">
43
+ <article class="card">
44
+ <div class="section-heading"><h2>Sentiment Playground</h2><span class="badge info">POST /api/hf/models/sentiment</span></div>
45
+ <div class="form-field">
46
+ <label for="sentiment-model">Sentiment model</label>
47
+ <select id="sentiment-model">
48
+ <option value="auto">auto (ensemble)</option>
49
+ <option value="cryptobert">cryptobert</option>
50
+ <option value="cryptobert_finbert">cryptobert_finbert</option>
51
+ <option value="tiny_crypto_lm">tiny_crypto_lm</option>
52
+ </select>
53
+ </div>
54
+ <div class="form-field">
55
+ <label for="sentiment-texts">Texts (one per line)</label>
56
+ <textarea id="sentiment-texts" rows="5" placeholder="BTC is breaking out...\nETH looks weak..."></textarea>
57
+ </div>
58
+ <button class="primary" id="run-sentiment">Run Sentiment</button>
59
+ <div id="sentiment-results" class="ws-stream" style="margin-top:16px;"></div>
60
+ </article>
61
+ <article class="card">
62
+ <div class="section-heading"><h2>Forecast Sandbox</h2><span class="badge info">POST /api/hf/models/forecast</span></div>
63
+ <div class="form-field">
64
+ <label for="forecast-model">Model</label>
65
+ <select id="forecast-model">
66
+ <option value="btc_lstm">btc_lstm</option>
67
+ <option value="btc_arima">btc_arima</option>
68
+ </select>
69
+ </div>
70
+ <div class="form-field">
71
+ <label for="forecast-series">Closing Prices (comma separated)</label>
72
+ <textarea id="forecast-series" rows="5" placeholder="67650, 67820, 68010, 68120"></textarea>
73
+ </div>
74
+ <div class="form-field">
75
+ <label for="forecast-steps">Future Steps</label>
76
+ <input type="number" id="forecast-steps" value="3" min="1" max="10" />
77
+ </div>
78
+ <button class="primary" id="run-forecast">Forecast</button>
79
+ <div id="forecast-results" class="ws-stream" style="margin-top:16px;"></div>
80
+ </article>
81
+ </section>
82
+
83
+ <section class="card">
84
+ <div class="section-heading">
85
+ <h2>HF Datasets</h2>
86
+ <span class="badge info">GET /api/hf/datasets/*</span>
87
+ </div>
88
+ <div class="button-row" style="margin-bottom:16px;">
89
+ <button class="secondary" data-dataset="market-ohlcv">Market OHLCV</button>
90
+ <button class="secondary" data-dataset="market-btc">BTC Technicals</button>
91
+ <button class="secondary" data-dataset="news-semantic">News Semantic</button>
92
+ </div>
93
+ <div id="dataset-output" class="ws-stream"></div>
94
+ </section>
95
+ </main>
96
+ </body>
97
+ </html>
archive_html/improved_dashboard.html ADDED
@@ -0,0 +1,443 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Monitor - Complete Overview</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1600px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ .header {
28
+ background: white;
29
+ border-radius: 15px;
30
+ padding: 30px;
31
+ margin-bottom: 20px;
32
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
33
+ }
34
+
35
+ .header h1 {
36
+ color: #667eea;
37
+ font-size: 2.5em;
38
+ margin-bottom: 10px;
39
+ }
40
+
41
+ .header p {
42
+ color: #666;
43
+ font-size: 1.1em;
44
+ }
45
+
46
+ .stats-grid {
47
+ display: grid;
48
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
49
+ gap: 20px;
50
+ margin-bottom: 20px;
51
+ }
52
+
53
+ .stat-card {
54
+ background: white;
55
+ border-radius: 15px;
56
+ padding: 25px;
57
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
58
+ transition: transform 0.3s ease;
59
+ }
60
+
61
+ .stat-card:hover {
62
+ transform: translateY(-5px);
63
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
64
+ }
65
+
66
+ .stat-card h3 {
67
+ color: #999;
68
+ font-size: 0.9em;
69
+ text-transform: uppercase;
70
+ margin-bottom: 10px;
71
+ font-weight: 600;
72
+ }
73
+
74
+ .stat-card .value {
75
+ font-size: 2.5em;
76
+ font-weight: bold;
77
+ margin-bottom: 5px;
78
+ }
79
+
80
+ .stat-card .label {
81
+ color: #666;
82
+ font-size: 0.9em;
83
+ }
84
+
85
+ .stat-card.green .value { color: #10b981; }
86
+ .stat-card.blue .value { color: #3b82f6; }
87
+ .stat-card.purple .value { color: #8b5cf6; }
88
+ .stat-card.orange .value { color: #f59e0b; }
89
+ .stat-card.red .value { color: #ef4444; }
90
+
91
+ .main-grid {
92
+ display: grid;
93
+ grid-template-columns: 2fr 1fr;
94
+ gap: 20px;
95
+ margin-bottom: 20px;
96
+ }
97
+
98
+ .card {
99
+ background: white;
100
+ border-radius: 15px;
101
+ padding: 25px;
102
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
103
+ }
104
+
105
+ .card h2 {
106
+ color: #333;
107
+ margin-bottom: 20px;
108
+ font-size: 1.5em;
109
+ border-bottom: 3px solid #667eea;
110
+ padding-bottom: 10px;
111
+ }
112
+
113
+ .providers-grid {
114
+ display: grid;
115
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
116
+ gap: 15px;
117
+ max-height: 500px;
118
+ overflow-y: auto;
119
+ }
120
+
121
+ .provider-card {
122
+ background: #f8f9fa;
123
+ border-radius: 10px;
124
+ padding: 15px;
125
+ border-left: 4px solid #ddd;
126
+ transition: all 0.3s ease;
127
+ }
128
+
129
+ .provider-card:hover {
130
+ transform: translateX(5px);
131
+ box-shadow: 0 3px 10px rgba(0,0,0,0.1);
132
+ }
133
+
134
+ .provider-card.online { border-left-color: #10b981; background: #f0fdf4; }
135
+ .provider-card.offline { border-left-color: #ef4444; background: #fef2f2; }
136
+ .provider-card.degraded { border-left-color: #f59e0b; background: #fffbeb; }
137
+
138
+ .provider-card .name {
139
+ font-weight: 600;
140
+ color: #333;
141
+ margin-bottom: 5px;
142
+ }
143
+
144
+ .provider-card .category {
145
+ font-size: 0.85em;
146
+ color: #666;
147
+ margin-bottom: 5px;
148
+ }
149
+
150
+ .provider-card .status {
151
+ font-size: 0.8em;
152
+ padding: 3px 8px;
153
+ border-radius: 5px;
154
+ display: inline-block;
155
+ font-weight: 600;
156
+ }
157
+
158
+ .status.online { background: #10b981; color: white; }
159
+ .status.offline { background: #ef4444; color: white; }
160
+ .status.degraded { background: #f59e0b; color: white; }
161
+
162
+ .category-list {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 15px;
166
+ }
167
+
168
+ .category-item {
169
+ background: #f8f9fa;
170
+ border-radius: 10px;
171
+ padding: 15px;
172
+ border-left: 4px solid #667eea;
173
+ }
174
+
175
+ .category-item .cat-name {
176
+ font-weight: 600;
177
+ color: #333;
178
+ margin-bottom: 10px;
179
+ font-size: 1.1em;
180
+ }
181
+
182
+ .category-item .cat-stats {
183
+ display: flex;
184
+ gap: 15px;
185
+ font-size: 0.9em;
186
+ }
187
+
188
+ .category-item .cat-stat {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 5px;
192
+ }
193
+
194
+ .chart-container {
195
+ position: relative;
196
+ height: 300px;
197
+ margin-top: 20px;
198
+ }
199
+
200
+ .loading {
201
+ text-align: center;
202
+ padding: 40px;
203
+ color: #666;
204
+ font-size: 1.2em;
205
+ }
206
+
207
+ .refresh-btn {
208
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
209
+ color: white;
210
+ border: none;
211
+ padding: 12px 30px;
212
+ border-radius: 10px;
213
+ font-size: 1em;
214
+ font-weight: 600;
215
+ cursor: pointer;
216
+ transition: all 0.3s ease;
217
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
218
+ }
219
+
220
+ .refresh-btn:hover {
221
+ transform: translateY(-2px);
222
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
223
+ }
224
+
225
+ .refresh-btn:active {
226
+ transform: translateY(0);
227
+ }
228
+
229
+ @keyframes pulse {
230
+ 0%, 100% { opacity: 1; }
231
+ 50% { opacity: 0.5; }
232
+ }
233
+
234
+ .updating {
235
+ animation: pulse 1.5s ease-in-out infinite;
236
+ }
237
+
238
+ .full-width {
239
+ grid-column: 1 / -1;
240
+ }
241
+
242
+ @media (max-width: 768px) {
243
+ .main-grid {
244
+ grid-template-columns: 1fr;
245
+ }
246
+ .stats-grid {
247
+ grid-template-columns: 1fr;
248
+ }
249
+ }
250
+ </style>
251
+ </head>
252
+ <body>
253
+ <div class="container">
254
+ <div class="header">
255
+ <h1>🚀 Crypto API Monitor</h1>
256
+ <p>Complete Real-Time Overview of All Cryptocurrency Data Sources</p>
257
+ <button class="refresh-btn" onclick="refreshAll()">🔄 Refresh Data</button>
258
+ </div>
259
+
260
+ <div class="stats-grid">
261
+ <div class="stat-card green">
262
+ <h3>Total Providers</h3>
263
+ <div class="value" id="totalProviders">-</div>
264
+ <div class="label">API Sources</div>
265
+ </div>
266
+ <div class="stat-card blue">
267
+ <h3>Online</h3>
268
+ <div class="value" id="onlineProviders">-</div>
269
+ <div class="label">Active & Working</div>
270
+ </div>
271
+ <div class="stat-card orange">
272
+ <h3>Degraded</h3>
273
+ <div class="value" id="degradedProviders">-</div>
274
+ <div class="label">Slow Response</div>
275
+ </div>
276
+ <div class="stat-card red">
277
+ <h3>Offline</h3>
278
+ <div class="value" id="offlineProviders">-</div>
279
+ <div class="label">Not Responding</div>
280
+ </div>
281
+ <div class="stat-card purple">
282
+ <h3>Categories</h3>
283
+ <div class="value" id="totalCategories">-</div>
284
+ <div class="label">Data Types</div>
285
+ </div>
286
+ <div class="stat-card green">
287
+ <h3>Uptime</h3>
288
+ <div class="value" id="uptimePercent">-</div>
289
+ <div class="label">Overall Health</div>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="main-grid">
294
+ <div class="card">
295
+ <h2>📊 All Providers Status</h2>
296
+ <div class="providers-grid" id="providersGrid">
297
+ <div class="loading">Loading providers...</div>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="card">
302
+ <h2>📁 Categories</h2>
303
+ <div class="category-list" id="categoryList">
304
+ <div class="loading">Loading categories...</div>
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="card full-width">
310
+ <h2>📈 Status Distribution</h2>
311
+ <div class="chart-container">
312
+ <canvas id="statusChart"></canvas>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ <script>
318
+ let statusChart = null;
319
+
320
+ async function loadProviders() {
321
+ try {
322
+ const response = await fetch('/api/providers');
323
+ const providers = await response.json();
324
+
325
+ // Update stats
326
+ const online = providers.filter(p => p.status === 'online').length;
327
+ const offline = providers.filter(p => p.status === 'offline').length;
328
+ const degraded = providers.filter(p => p.status === 'degraded').length;
329
+ const uptime = ((online / providers.length) * 100).toFixed(1);
330
+
331
+ document.getElementById('totalProviders').textContent = providers.length;
332
+ document.getElementById('onlineProviders').textContent = online;
333
+ document.getElementById('degradedProviders').textContent = degraded;
334
+ document.getElementById('offlineProviders').textContent = offline;
335
+ document.getElementById('uptimePercent').textContent = uptime + '%';
336
+
337
+ // Group by category
338
+ const categories = {};
339
+ providers.forEach(p => {
340
+ if (!categories[p.category]) {
341
+ categories[p.category] = { total: 0, online: 0, offline: 0, degraded: 0 };
342
+ }
343
+ categories[p.category].total++;
344
+ categories[p.category][p.status]++;
345
+ });
346
+
347
+ document.getElementById('totalCategories').textContent = Object.keys(categories).length;
348
+
349
+ // Display providers
350
+ const providersGrid = document.getElementById('providersGrid');
351
+ providersGrid.innerHTML = providers.map(p => `
352
+ <div class="provider-card ${p.status}">
353
+ <div class="name">${p.name}</div>
354
+ <div class="category">${p.category}</div>
355
+ <span class="status ${p.status}">${p.status.toUpperCase()}</span>
356
+ ${p.response_time_ms ? `<div style="font-size: 0.8em; color: #666; margin-top: 5px;">${Math.round(p.response_time_ms)}ms</div>` : ''}
357
+ </div>
358
+ `).join('');
359
+
360
+ // Display categories
361
+ const categoryList = document.getElementById('categoryList');
362
+ categoryList.innerHTML = Object.entries(categories).map(([name, stats]) => `
363
+ <div class="category-item">
364
+ <div class="cat-name">${name}</div>
365
+ <div class="cat-stats">
366
+ <div class="cat-stat">
367
+ <span style="color: #10b981;">●</span>
368
+ <span>${stats.online} online</span>
369
+ </div>
370
+ <div class="cat-stat">
371
+ <span style="color: #f59e0b;">●</span>
372
+ <span>${stats.degraded} degraded</span>
373
+ </div>
374
+ <div class="cat-stat">
375
+ <span style="color: #ef4444;">●</span>
376
+ <span>${stats.offline} offline</span>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ `).join('');
381
+
382
+ // Update chart
383
+ updateChart(online, degraded, offline);
384
+
385
+ } catch (error) {
386
+ console.error('Error loading providers:', error);
387
+ document.getElementById('providersGrid').innerHTML = '<div class="loading">Error loading data</div>';
388
+ }
389
+ }
390
+
391
+ function updateChart(online, degraded, offline) {
392
+ const ctx = document.getElementById('statusChart').getContext('2d');
393
+
394
+ if (statusChart) {
395
+ statusChart.destroy();
396
+ }
397
+
398
+ statusChart = new Chart(ctx, {
399
+ type: 'doughnut',
400
+ data: {
401
+ labels: ['Online', 'Degraded', 'Offline'],
402
+ datasets: [{
403
+ data: [online, degraded, offline],
404
+ backgroundColor: ['#10b981', '#f59e0b', '#ef4444'],
405
+ borderWidth: 0
406
+ }]
407
+ },
408
+ options: {
409
+ responsive: true,
410
+ maintainAspectRatio: false,
411
+ plugins: {
412
+ legend: {
413
+ position: 'bottom',
414
+ labels: {
415
+ font: { size: 14 },
416
+ padding: 20
417
+ }
418
+ }
419
+ }
420
+ }
421
+ });
422
+ }
423
+
424
+ async function refreshAll() {
425
+ const btn = document.querySelector('.refresh-btn');
426
+ btn.classList.add('updating');
427
+ btn.textContent = '⏳ Refreshing...';
428
+
429
+ await loadProviders();
430
+
431
+ btn.classList.remove('updating');
432
+ btn.textContent = '🔄 Refresh Data';
433
+ }
434
+
435
+ // Load on page load
436
+ loadProviders();
437
+
438
+ // Auto-refresh every 30 seconds
439
+ setInterval(loadProviders, 30000);
440
+ </script>
441
+ </body>
442
+ </html>
443
+
archive_html/index (1).html ADDED
The diff for this file is too large to render. See raw diff
 
archive_html/index_backup.html ADDED
@@ -0,0 +1,2452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Real-time Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&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
+ :root {
11
+ --bg-primary: #ffffff;
12
+ --bg-secondary: #f8f9ff;
13
+ --bg-card: #ffffff;
14
+ --bg-hover: #f3f4ff;
15
+ --text-primary: #1e1b4b;
16
+ --text-secondary: #4c4380;
17
+ --text-muted: #7c3aed;
18
+ --accent-primary: #8b5cf6;
19
+ --accent-secondary: #a78bfa;
20
+ --accent-tertiary: #c084fc;
21
+ --purple-glow: rgba(139, 92, 246, 0.5);
22
+ --success: #10b981;
23
+ --success-bg: #d1fae5;
24
+ --warning: #f59e0b;
25
+ --warning-bg: #fef3c7;
26
+ --danger: #ef4444;
27
+ --danger-bg: #fee2e2;
28
+ --info: #06b6d4;
29
+ --info-bg: #cffafe;
30
+ --border: rgba(139, 92, 246, 0.2);
31
+ --shadow-sm: 0 2px 8px rgba(139, 92, 246, 0.08);
32
+ --shadow: 0 4px 16px rgba(139, 92, 246, 0.12);
33
+ --shadow-lg: 0 10px 40px rgba(139, 92, 246, 0.15);
34
+ --radius: 16px;
35
+ --radius-lg: 24px;
36
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
37
+ }
38
+
39
+ * {
40
+ margin: 0;
41
+ padding: 0;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ body {
46
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
47
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 50%, #f0f4ff 100%);
48
+ background-attachment: fixed;
49
+ color: var(--text-primary);
50
+ line-height: 1.6;
51
+ min-height: 100vh;
52
+ }
53
+
54
+ body::before {
55
+ content: '';
56
+ position: fixed;
57
+ top: 0;
58
+ left: 0;
59
+ right: 0;
60
+ bottom: 0;
61
+ background:
62
+ radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.05) 0%, transparent 50%),
63
+ radial-gradient(circle at 80% 70%, rgba(168, 85, 247, 0.05) 0%, transparent 50%);
64
+ pointer-events: none;
65
+ z-index: 0;
66
+ }
67
+
68
+ .container {
69
+ max-width: 1600px;
70
+ margin: 0 auto;
71
+ padding: 24px;
72
+ position: relative;
73
+ z-index: 1;
74
+ }
75
+
76
+ /* Header */
77
+ .header {
78
+ background: var(--bg-card);
79
+ border: 2px solid var(--border);
80
+ border-radius: var(--radius-lg);
81
+ padding: 28px;
82
+ margin-bottom: 24px;
83
+ box-shadow: var(--shadow-lg);
84
+ }
85
+
86
+ .header-top {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: space-between;
90
+ flex-wrap: wrap;
91
+ gap: 20px;
92
+ margin-bottom: 24px;
93
+ }
94
+
95
+ .logo {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 16px;
99
+ }
100
+
101
+ .logo-icon {
102
+ width: 64px;
103
+ height: 64px;
104
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
105
+ border-radius: 20px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ box-shadow: 0 10px 30px rgba(139, 92, 246, 0.4);
110
+ position: relative;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .logo-icon::after {
115
+ content: '';
116
+ position: absolute;
117
+ top: -50%;
118
+ left: -50%;
119
+ right: -50%;
120
+ bottom: -50%;
121
+ background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
122
+ animation: shimmer 3s infinite;
123
+ }
124
+
125
+ @keyframes shimmer {
126
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
127
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
128
+ }
129
+
130
+ .logo-text h1 {
131
+ font-size: 32px;
132
+ font-weight: 900;
133
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
134
+ -webkit-background-clip: text;
135
+ -webkit-text-fill-color: transparent;
136
+ background-clip: text;
137
+ margin-bottom: 4px;
138
+ letter-spacing: -0.5px;
139
+ }
140
+
141
+ .logo-text p {
142
+ font-size: 14px;
143
+ color: var(--text-muted);
144
+ font-weight: 600;
145
+ }
146
+
147
+ .header-actions {
148
+ display: flex;
149
+ gap: 12px;
150
+ align-items: center;
151
+ flex-wrap: wrap;
152
+ }
153
+
154
+ .status-badge {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 10px;
158
+ padding: 12px 20px;
159
+ border-radius: 999px;
160
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(16, 185, 129, 0.08) 100%);
161
+ border: 2px solid rgba(16, 185, 129, 0.4);
162
+ font-size: 14px;
163
+ font-weight: 700;
164
+ color: var(--success);
165
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.5px;
168
+ }
169
+
170
+ .status-dot {
171
+ width: 10px;
172
+ height: 10px;
173
+ border-radius: 50%;
174
+ background: var(--success);
175
+ animation: pulse-glow 2s infinite;
176
+ }
177
+
178
+ @keyframes pulse-glow {
179
+ 0%, 100% {
180
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7),
181
+ 0 0 10px rgba(16, 185, 129, 0.5);
182
+ }
183
+ 50% {
184
+ box-shadow: 0 0 0 8px rgba(16, 185, 129, 0),
185
+ 0 0 20px rgba(16, 185, 129, 0.3);
186
+ }
187
+ }
188
+
189
+ .connection-status {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ padding: 10px 16px;
194
+ border-radius: 999px;
195
+ background: var(--bg-card);
196
+ border: 2px solid var(--border);
197
+ font-size: 12px;
198
+ font-weight: 700;
199
+ }
200
+
201
+ .connection-status.connected { border-color: var(--success); color: var(--success); }
202
+ .connection-status.disconnected { border-color: var(--danger); color: var(--danger); }
203
+ .connection-status.connecting { border-color: var(--warning); color: var(--warning); }
204
+
205
+ .btn {
206
+ padding: 14px 28px;
207
+ border-radius: 14px;
208
+ border: none;
209
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
210
+ color: white;
211
+ font-family: inherit;
212
+ font-size: 14px;
213
+ font-weight: 700;
214
+ cursor: pointer;
215
+ transition: var(--transition);
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.5px;
222
+ position: relative;
223
+ overflow: hidden;
224
+ }
225
+
226
+ .btn::before {
227
+ content: '';
228
+ position: absolute;
229
+ top: 0;
230
+ left: -100%;
231
+ width: 100%;
232
+ height: 100%;
233
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
234
+ transition: left 0.5s;
235
+ }
236
+
237
+ .btn:hover {
238
+ transform: translateY(-3px);
239
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5);
240
+ }
241
+
242
+ .btn:hover::before {
243
+ left: 100%;
244
+ }
245
+
246
+ .btn-secondary {
247
+ background: white;
248
+ color: var(--accent-primary);
249
+ border: 2px solid var(--border);
250
+ box-shadow: var(--shadow-sm);
251
+ }
252
+
253
+ .btn-secondary:hover {
254
+ background: var(--bg-hover);
255
+ border-color: var(--accent-primary);
256
+ }
257
+
258
+ .btn-icon {
259
+ padding: 12px;
260
+ width: 44px;
261
+ height: 44px;
262
+ }
263
+
264
+ .icon {
265
+ width: 20px;
266
+ height: 20px;
267
+ stroke: currentColor;
268
+ stroke-width: 2.5;
269
+ stroke-linecap: round;
270
+ stroke-linejoin: round;
271
+ fill: none;
272
+ }
273
+
274
+ .icon-lg {
275
+ width: 26px;
276
+ height: 26px;
277
+ }
278
+
279
+ /* KPI Cards */
280
+ .kpi-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
283
+ gap: 20px;
284
+ margin-bottom: 24px;
285
+ }
286
+
287
+ .kpi-card {
288
+ background: var(--bg-card);
289
+ border: 2px solid var(--border);
290
+ border-radius: var(--radius-lg);
291
+ padding: 32px;
292
+ transition: var(--transition);
293
+ box-shadow: var(--shadow);
294
+ position: relative;
295
+ overflow: hidden;
296
+ cursor: pointer;
297
+ }
298
+
299
+ .kpi-card::before {
300
+ content: '';
301
+ position: absolute;
302
+ top: 0;
303
+ left: 0;
304
+ right: 0;
305
+ height: 6px;
306
+ background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
307
+ transform: scaleX(0);
308
+ transform-origin: left;
309
+ transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
310
+ }
311
+
312
+ .kpi-card:hover {
313
+ transform: translateY(-8px) scale(1.02);
314
+ box-shadow: 0 16px 48px rgba(139, 92, 246, 0.25);
315
+ border-color: var(--accent-primary);
316
+ }
317
+
318
+ .kpi-card:hover::before {
319
+ transform: scaleX(1);
320
+ }
321
+
322
+ .kpi-header {
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: space-between;
326
+ margin-bottom: 20px;
327
+ }
328
+
329
+ .kpi-label {
330
+ font-size: 12px;
331
+ color: var(--text-muted);
332
+ font-weight: 800;
333
+ text-transform: uppercase;
334
+ letter-spacing: 1.2px;
335
+ }
336
+
337
+ .kpi-icon-wrapper {
338
+ width: 64px;
339
+ height: 64px;
340
+ border-radius: 18px;
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ transition: var(--transition);
345
+ box-shadow: var(--shadow);
346
+ }
347
+
348
+ .kpi-card:hover .kpi-icon-wrapper {
349
+ transform: rotate(-5deg) scale(1.15);
350
+ box-shadow: 0 10px 30px rgba(139, 92, 246, 0.3);
351
+ }
352
+
353
+ .kpi-value {
354
+ font-size: 48px;
355
+ font-weight: 900;
356
+ margin-bottom: 16px;
357
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
358
+ -webkit-background-clip: text;
359
+ -webkit-text-fill-color: transparent;
360
+ background-clip: text;
361
+ line-height: 1;
362
+ animation: countUp 0.6s ease-out;
363
+ letter-spacing: -2px;
364
+ }
365
+
366
+ @keyframes countUp {
367
+ from { opacity: 0; transform: translateY(20px); }
368
+ to { opacity: 1; transform: translateY(0); }
369
+ }
370
+
371
+ .kpi-trend {
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 10px;
375
+ font-size: 13px;
376
+ font-weight: 700;
377
+ padding: 8px 16px;
378
+ border-radius: 12px;
379
+ width: fit-content;
380
+ text-transform: uppercase;
381
+ letter-spacing: 0.5px;
382
+ }
383
+
384
+ .trend-up {
385
+ color: var(--success);
386
+ background: var(--success-bg);
387
+ border: 2px solid var(--success);
388
+ }
389
+
390
+ .trend-down {
391
+ color: var(--danger);
392
+ background: var(--danger-bg);
393
+ border: 2px solid var(--danger);
394
+ }
395
+
396
+ .trend-neutral {
397
+ color: var(--info);
398
+ background: var(--info-bg);
399
+ border: 2px solid var(--info);
400
+ }
401
+
402
+ /* Tabs */
403
+ .tabs {
404
+ display: flex;
405
+ gap: 6px;
406
+ margin-bottom: 24px;
407
+ overflow-x: auto;
408
+ padding: 8px;
409
+ background: var(--bg-card);
410
+ border-radius: var(--radius-lg);
411
+ border: 2px solid var(--border);
412
+ box-shadow: var(--shadow-sm);
413
+ }
414
+
415
+ .tab {
416
+ padding: 12px 20px;
417
+ border-radius: 12px;
418
+ background: transparent;
419
+ border: none;
420
+ color: var(--text-secondary);
421
+ cursor: pointer;
422
+ transition: all 0.25s;
423
+ white-space: nowrap;
424
+ font-weight: 700;
425
+ font-size: 13px;
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 8px;
429
+ }
430
+
431
+ .tab:hover:not(.active) {
432
+ background: var(--bg-hover);
433
+ color: var(--text-primary);
434
+ }
435
+
436
+ .tab.active {
437
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
438
+ color: white;
439
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
440
+ transform: scale(1.05);
441
+ }
442
+
443
+ .tab .icon {
444
+ width: 16px;
445
+ height: 16px;
446
+ }
447
+
448
+ /* Tab Content */
449
+ .tab-content {
450
+ display: none;
451
+ animation: fadeIn 0.4s ease;
452
+ }
453
+
454
+ .tab-content.active {
455
+ display: block;
456
+ }
457
+
458
+ @keyframes fadeIn {
459
+ from { opacity: 0; transform: translateY(20px); }
460
+ to { opacity: 1; transform: translateY(0); }
461
+ }
462
+
463
+ /* Card */
464
+ .card {
465
+ background: var(--bg-card);
466
+ border: 2px solid var(--border);
467
+ border-radius: var(--radius-lg);
468
+ padding: 28px;
469
+ margin-bottom: 24px;
470
+ box-shadow: var(--shadow);
471
+ transition: var(--transition);
472
+ }
473
+
474
+ .card:hover {
475
+ box-shadow: var(--shadow-lg);
476
+ }
477
+
478
+ .card-header {
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: space-between;
482
+ margin-bottom: 24px;
483
+ padding-bottom: 16px;
484
+ border-bottom: 2px solid var(--border);
485
+ }
486
+
487
+ .card-title {
488
+ font-size: 20px;
489
+ font-weight: 800;
490
+ display: flex;
491
+ align-items: center;
492
+ gap: 12px;
493
+ color: var(--text-primary);
494
+ }
495
+
496
+ .card-actions {
497
+ display: flex;
498
+ gap: 8px;
499
+ }
500
+
501
+ /* Table */
502
+ .table-container {
503
+ overflow-x: auto;
504
+ border-radius: var(--radius);
505
+ border: 2px solid var(--border);
506
+ }
507
+
508
+ .table {
509
+ width: 100%;
510
+ border-collapse: collapse;
511
+ }
512
+
513
+ .table thead {
514
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
515
+ }
516
+
517
+ .table thead th {
518
+ color: white;
519
+ font-weight: 700;
520
+ font-size: 13px;
521
+ text-align: left;
522
+ padding: 16px;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.8px;
525
+ }
526
+
527
+ .table tbody tr {
528
+ transition: var(--transition);
529
+ border-bottom: 1px solid var(--border);
530
+ }
531
+
532
+ .table tbody tr:hover {
533
+ background: var(--bg-hover);
534
+ }
535
+
536
+ .table tbody td {
537
+ padding: 16px;
538
+ font-size: 14px;
539
+ color: var(--text-secondary);
540
+ }
541
+
542
+ /* Badge */
543
+ .badge {
544
+ display: inline-flex;
545
+ align-items: center;
546
+ gap: 6px;
547
+ padding: 6px 12px;
548
+ border-radius: 999px;
549
+ font-size: 12px;
550
+ font-weight: 700;
551
+ white-space: nowrap;
552
+ }
553
+
554
+ .badge-success {
555
+ background: var(--success-bg);
556
+ color: var(--success);
557
+ border: 2px solid var(--success);
558
+ }
559
+
560
+ .badge-warning {
561
+ background: var(--warning-bg);
562
+ color: var(--warning);
563
+ border: 2px solid var(--warning);
564
+ }
565
+
566
+ .badge-danger {
567
+ background: var(--danger-bg);
568
+ color: var(--danger);
569
+ border: 2px solid var(--danger);
570
+ }
571
+
572
+ .badge-info {
573
+ background: var(--info-bg);
574
+ color: var(--info);
575
+ border: 2px solid var(--info);
576
+ }
577
+
578
+ /* Progress Bar */
579
+ .progress {
580
+ height: 12px;
581
+ background: var(--bg-hover);
582
+ border-radius: 999px;
583
+ overflow: hidden;
584
+ margin: 8px 0;
585
+ border: 2px solid var(--border);
586
+ }
587
+
588
+ .progress-bar {
589
+ height: 100%;
590
+ background: linear-gradient(90deg, #8b5cf6, #a78bfa);
591
+ border-radius: 999px;
592
+ transition: width 0.5s ease;
593
+ }
594
+
595
+ .progress-bar.success {
596
+ background: linear-gradient(90deg, var(--success), #34d399);
597
+ }
598
+
599
+ .progress-bar.warning {
600
+ background: linear-gradient(90deg, var(--warning), #fbbf24);
601
+ }
602
+
603
+ .progress-bar.danger {
604
+ background: linear-gradient(90deg, var(--danger), #f87171);
605
+ }
606
+
607
+ /* Chart Container */
608
+ .chart-container {
609
+ position: relative;
610
+ height: 320px;
611
+ margin: 20px 0;
612
+ background: var(--bg-secondary);
613
+ border-radius: var(--radius);
614
+ padding: 16px;
615
+ border: 2px solid var(--border);
616
+ }
617
+
618
+ /* Loading */
619
+ .loading-overlay {
620
+ position: fixed;
621
+ top: 0;
622
+ left: 0;
623
+ right: 0;
624
+ bottom: 0;
625
+ background: rgba(255, 255, 255, 0.95);
626
+ backdrop-filter: blur(8px);
627
+ display: none;
628
+ align-items: center;
629
+ justify-content: center;
630
+ z-index: 9999;
631
+ }
632
+
633
+ .loading-overlay.active {
634
+ display: flex;
635
+ }
636
+
637
+ .spinner {
638
+ width: 60px;
639
+ height: 60px;
640
+ border: 6px solid var(--border);
641
+ border-top-color: var(--accent-primary);
642
+ border-radius: 50%;
643
+ animation: spin 0.8s linear infinite;
644
+ }
645
+
646
+ @keyframes spin {
647
+ to { transform: rotate(360deg); }
648
+ }
649
+
650
+ .loading-inline {
651
+ display: flex;
652
+ align-items: center;
653
+ justify-content: center;
654
+ padding: 40px;
655
+ color: var(--text-muted);
656
+ }
657
+
658
+ .spinner-inline {
659
+ width: 32px;
660
+ height: 32px;
661
+ border: 3px solid var(--border);
662
+ border-top-color: var(--accent-primary);
663
+ border-radius: 50%;
664
+ animation: spin 0.8s linear infinite;
665
+ margin-right: 12px;
666
+ }
667
+
668
+ /* Toast */
669
+ .toast-container {
670
+ position: fixed;
671
+ bottom: 24px;
672
+ right: 24px;
673
+ z-index: 10000;
674
+ display: flex;
675
+ flex-direction: column;
676
+ gap: 12px;
677
+ max-width: 400px;
678
+ }
679
+
680
+ .toast {
681
+ padding: 16px 20px;
682
+ border-radius: var(--radius);
683
+ background: var(--bg-card);
684
+ border: 2px solid var(--border);
685
+ box-shadow: var(--shadow-lg);
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 12px;
689
+ animation: slideInRight 0.3s ease;
690
+ min-width: 300px;
691
+ }
692
+
693
+ @keyframes slideInRight {
694
+ from { transform: translateX(400px); opacity: 0; }
695
+ to { transform: translateX(0); opacity: 1; }
696
+ }
697
+
698
+ .toast.success { border-color: var(--success); background: var(--success-bg); }
699
+ .toast.error { border-color: var(--danger); background: var(--danger-bg); }
700
+ .toast.warning { border-color: var(--warning); background: var(--warning-bg); }
701
+ .toast.info { border-color: var(--info); background: var(--info-bg); }
702
+
703
+ .toast-content { flex: 1; }
704
+ .toast-title { font-weight: 700; font-size: 14px; margin-bottom: 2px; }
705
+ .toast-message { font-size: 13px; color: var(--text-secondary); }
706
+
707
+ /* Alert */
708
+ .alert {
709
+ padding: 18px 24px;
710
+ border-radius: var(--radius);
711
+ margin-bottom: 16px;
712
+ display: flex;
713
+ align-items: flex-start;
714
+ gap: 14px;
715
+ border-left: 6px solid;
716
+ box-shadow: var(--shadow-sm);
717
+ }
718
+
719
+ .alert-success { background: var(--success-bg); border-color: var(--success); color: var(--success); }
720
+ .alert-warning { background: var(--warning-bg); border-color: var(--warning); color: var(--warning); }
721
+ .alert-danger { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
722
+ .alert-info { background: var(--info-bg); border-color: var(--info); color: var(--info); }
723
+
724
+ .alert-content { flex: 1; }
725
+ .alert-title { font-weight: 800; margin-bottom: 6px; font-size: 15px; }
726
+ .alert-message { font-size: 14px; opacity: 0.9; }
727
+
728
+ /* Grid */
729
+ .grid { display: grid; gap: 20px; }
730
+ .grid-2 { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
731
+ .grid-3 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
732
+
733
+ /* Input */
734
+ .input {
735
+ width: 100%;
736
+ padding: 12px 16px;
737
+ border-radius: var(--radius);
738
+ border: 2px solid var(--border);
739
+ background: var(--bg-card);
740
+ color: var(--text-primary);
741
+ font-family: inherit;
742
+ font-size: 14px;
743
+ transition: var(--transition);
744
+ }
745
+
746
+ .input:focus {
747
+ outline: none;
748
+ border-color: var(--accent-primary);
749
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
750
+ }
751
+
752
+ /* Responsive */
753
+ @media (max-width: 768px) {
754
+ .container { padding: 16px; }
755
+ .header-top { flex-direction: column; align-items: flex-start; }
756
+ .kpi-grid { grid-template-columns: 1fr; }
757
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
758
+ .card-header { flex-direction: column; align-items: flex-start; gap: 16px; }
759
+ .card-actions { width: 100%; justify-content: flex-end; }
760
+ }
761
+ </style>
762
+ </head>
763
+ <body>
764
+ <div class="loading-overlay" id="loadingOverlay">
765
+ <div class="spinner"></div>
766
+ </div>
767
+
768
+ <div class="toast-container" id="toastContainer"></div>
769
+
770
+ <div class="container">
771
+ <div class="header">
772
+ <div class="header-top">
773
+ <div class="logo">
774
+ <div class="logo-icon">
775
+ <svg class="icon icon-lg" style="stroke: white;">
776
+ <circle cx="12" cy="12" r="10"></circle>
777
+ <path d="M12 6v6l4 2"></path>
778
+ </svg>
779
+ </div>
780
+ <div class="logo-text">
781
+ <h1>Crypto API Monitor</h1>
782
+ <p>Real-time Cryptocurrency API Resource Monitoring</p>
783
+ </div>
784
+ </div>
785
+ <div class="header-actions">
786
+ <div class="connection-status" id="wsStatus">
787
+ <span class="status-dot"></span>
788
+ <span id="wsStatusText">Connecting...</span>
789
+ </div>
790
+ <div class="status-badge" id="systemStatus">
791
+ <span class="status-dot"></span>
792
+ <span id="systemStatusText">System Active</span>
793
+ </div>
794
+ <button class="btn" onclick="refreshAll()">
795
+ <svg class="icon">
796
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
797
+ </svg>
798
+ Refresh All
799
+ </button>
800
+ </div>
801
+ </div>
802
+
803
+ <div class="kpi-grid" id="kpiGrid">
804
+ <div class="kpi-card">
805
+ <div class="kpi-header">
806
+ <span class="kpi-label">Total APIs</span>
807
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.1) 100%);">
808
+ <svg class="icon icon-lg" style="stroke: #3b82f6;">
809
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
810
+ <line x1="3" y1="9" x2="21" y2="9"></line>
811
+ <line x1="9" y1="21" x2="9" y2="9"></line>
812
+ </svg>
813
+ </div>
814
+ </div>
815
+ <div class="kpi-value" id="kpiTotalAPIs">--</div>
816
+ <div class="kpi-trend trend-neutral">
817
+ <svg class="icon" style="width: 16px; height: 16px;">
818
+ <path d="M12 20V10M18 20V4M6 20v-4"></path>
819
+ </svg>
820
+ <span id="kpiTotalTrend">Loading...</span>
821
+ </div>
822
+ </div>
823
+
824
+ <div class="kpi-card">
825
+ <div class="kpi-header">
826
+ <span class="kpi-label">Online</span>
827
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.1) 100%);">
828
+ <svg class="icon icon-lg" style="stroke: #10b981;">
829
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
830
+ <polyline points="9 12 11 14 15 10"></polyline>
831
+ </svg>
832
+ </div>
833
+ </div>
834
+ <div class="kpi-value" id="kpiOnline">--</div>
835
+ <div class="kpi-trend trend-up">
836
+ <svg class="icon" style="width: 16px; height: 16px;">
837
+ <line x1="12" y1="19" x2="12" y2="5"></line>
838
+ <polyline points="5 12 12 5 19 12"></polyline>
839
+ </svg>
840
+ <span id="kpiOnlineTrend">Loading...</span>
841
+ </div>
842
+ </div>
843
+
844
+ <div class="kpi-card">
845
+ <div class="kpi-header">
846
+ <span class="kpi-label">Avg Response</span>
847
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(217, 119, 6, 0.1) 100%);">
848
+ <svg class="icon icon-lg" style="stroke: #f59e0b;">
849
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
850
+ </svg>
851
+ </div>
852
+ </div>
853
+ <div class="kpi-value" id="kpiAvgResponse" style="font-size: 32px;">--</div>
854
+ <div class="kpi-trend trend-down">
855
+ <svg class="icon" style="width: 16px; height: 16px;">
856
+ <line x1="12" y1="5" x2="12" y2="19"></line>
857
+ <polyline points="19 12 12 19 5 12"></polyline>
858
+ </svg>
859
+ <span id="kpiResponseTrend">Loading...</span>
860
+ </div>
861
+ </div>
862
+
863
+ <div class="kpi-card">
864
+ <div class="kpi-header">
865
+ <span class="kpi-label">Last Update</span>
866
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(124, 58, 237, 0.1) 100%);">
867
+ <svg class="icon icon-lg" style="stroke: #8b5cf6;">
868
+ <circle cx="12" cy="12" r="10"></circle>
869
+ <polyline points="12 6 12 12 16 14"></polyline>
870
+ </svg>
871
+ </div>
872
+ </div>
873
+ <div class="kpi-value" id="kpiLastUpdate" style="font-size: 20px; line-height: 1.2;">--</div>
874
+ <div class="kpi-trend trend-neutral">
875
+ <svg class="icon" style="width: 16px; height: 16px;">
876
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
877
+ </svg>
878
+ <span>Auto-refresh enabled</span>
879
+ </div>
880
+ </div>
881
+ </div>
882
+ </div>
883
+
884
+ <div class="tabs">
885
+ <div class="tab active" onclick="switchTab(event, 'dashboard')">
886
+ <svg class="icon">
887
+ <rect x="3" y="3" width="7" height="7"></rect>
888
+ <rect x="14" y="3" width="7" height="7"></rect>
889
+ <rect x="14" y="14" width="7" height="7"></rect>
890
+ <rect x="3" y="14" width="7" height="7"></rect>
891
+ </svg>
892
+ <span>Dashboard</span>
893
+ </div>
894
+ <div class="tab" onclick="switchTab(event, 'providers')">
895
+ <svg class="icon">
896
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
897
+ <polyline points="14 2 14 8 20 8"></polyline>
898
+ </svg>
899
+ <span>Providers</span>
900
+ </div>
901
+ <div class="tab" onclick="switchTab(event, 'categories')">
902
+ <svg class="icon">
903
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
904
+ <line x1="3" y1="9" x2="21" y2="9"></line>
905
+ <line x1="9" y1="21" x2="9" y2="9"></line>
906
+ </svg>
907
+ <span>Categories</span>
908
+ </div>
909
+ <div class="tab" onclick="switchTab(event, 'ratelimits')">
910
+ <svg class="icon">
911
+ <circle cx="12" cy="12" r="10"></circle>
912
+ <polyline points="12 6 12 12 16 14"></polyline>
913
+ </svg>
914
+ <span>Rate Limits</span>
915
+ </div>
916
+ <div class="tab" onclick="switchTab(event, 'logs')">
917
+ <svg class="icon">
918
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
919
+ <polyline points="14 2 14 8 20 8"></polyline>
920
+ <line x1="16" y1="13" x2="8" y2="13"></line>
921
+ </svg>
922
+ <span>Logs</span>
923
+ </div>
924
+ <div class="tab" onclick="switchTab(event, 'alerts')">
925
+ <svg class="icon">
926
+ <circle cx="12" cy="12" r="10"></circle>
927
+ <line x1="12" y1="8" x2="12" y2="12"></line>
928
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
929
+ </svg>
930
+ <span>Alerts</span>
931
+ </div>
932
+ <div class="tab" onclick="switchTab(event, 'huggingface')">
933
+ <svg class="icon">
934
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
935
+ <circle cx="12" cy="12" r="3"></circle>
936
+ </svg>
937
+ <span>HuggingFace</span>
938
+ </div>
939
+ </div>
940
+
941
+ <!-- Dashboard Tab -->
942
+ <div class="tab-content active" id="tab-dashboard">
943
+ <div id="alertsContainer"></div>
944
+
945
+ <div class="card">
946
+ <div class="card-header">
947
+ <h2 class="card-title">
948
+ <svg class="icon icon-lg">
949
+ <rect x="3" y="3" width="7" height="7"></rect>
950
+ <rect x="14" y="3" width="7" height="7"></rect>
951
+ </svg>
952
+ System Overview
953
+ </h2>
954
+ <button class="btn btn-secondary" onclick="loadProviders()">
955
+ <svg class="icon">
956
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
957
+ </svg>
958
+ Refresh
959
+ </button>
960
+ </div>
961
+ <div class="table-container">
962
+ <table class="table">
963
+ <thead>
964
+ <tr>
965
+ <th>Provider</th>
966
+ <th>Category</th>
967
+ <th>Status</th>
968
+ <th>Response Time</th>
969
+ <th>Last Check</th>
970
+ </tr>
971
+ </thead>
972
+ <tbody id="providersTableBody">
973
+ <tr>
974
+ <td colspan="5">
975
+ <div class="loading-inline">
976
+ <div class="spinner-inline"></div>
977
+ Loading providers...
978
+ </div>
979
+ </td>
980
+ </tr>
981
+ </tbody>
982
+ </table>
983
+ </div>
984
+ </div>
985
+
986
+ <div class="grid grid-2">
987
+ <div class="card">
988
+ <div class="card-header">
989
+ <h2 class="card-title">
990
+ <svg class="icon icon-lg">
991
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
992
+ </svg>
993
+ Health Status
994
+ </h2>
995
+ </div>
996
+ <div class="chart-container">
997
+ <canvas id="healthChart"></canvas>
998
+ </div>
999
+ </div>
1000
+
1001
+ <div class="card">
1002
+ <div class="card-header">
1003
+ <h2 class="card-title">
1004
+ <svg class="icon icon-lg">
1005
+ <path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path>
1006
+ </svg>
1007
+ Status Distribution
1008
+ </h2>
1009
+ </div>
1010
+ <div class="chart-container">
1011
+ <canvas id="statusChart"></canvas>
1012
+ </div>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+
1017
+ <!-- Providers Tab -->
1018
+ <div class="tab-content" id="tab-providers">
1019
+ <div class="card">
1020
+ <div class="card-header">
1021
+ <h2 class="card-title">
1022
+ <svg class="icon icon-lg">
1023
+ <circle cx="12" cy="12" r="10"></circle>
1024
+ </svg>
1025
+ All Providers
1026
+ </h2>
1027
+ <button class="btn btn-secondary" onclick="loadProviders()">
1028
+ <svg class="icon">
1029
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1030
+ </svg>
1031
+ Refresh
1032
+ </button>
1033
+ </div>
1034
+ <div id="providersDetail">
1035
+ <div class="loading-inline">
1036
+ <div class="spinner-inline"></div>
1037
+ Loading providers details...
1038
+ </div>
1039
+ </div>
1040
+ </div>
1041
+ </div>
1042
+
1043
+ <!-- Categories Tab -->
1044
+ <div class="tab-content" id="tab-categories">
1045
+ <div class="card">
1046
+ <div class="card-header">
1047
+ <h2 class="card-title">
1048
+ <svg class="icon icon-lg">
1049
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
1050
+ <line x1="3" y1="9" x2="21" y2="9"></line>
1051
+ </svg>
1052
+ Categories Overview
1053
+ </h2>
1054
+ <button class="btn btn-secondary" onclick="loadCategories()">
1055
+ <svg class="icon">
1056
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1057
+ </svg>
1058
+ Refresh
1059
+ </button>
1060
+ </div>
1061
+ <div class="table-container">
1062
+ <table class="table">
1063
+ <thead>
1064
+ <tr>
1065
+ <th>Category</th>
1066
+ <th>Total Sources</th>
1067
+ <th>Online</th>
1068
+ <th>Health %</th>
1069
+ <th>Avg Response</th>
1070
+ <th>Last Updated</th>
1071
+ <th>Status</th>
1072
+ </tr>
1073
+ </thead>
1074
+ <tbody id="categoriesTableBody">
1075
+ <tr>
1076
+ <td colspan="7">
1077
+ <div class="loading-inline">
1078
+ <div class="spinner-inline"></div>
1079
+ Loading categories...
1080
+ </div>
1081
+ </td>
1082
+ </tr>
1083
+ </tbody>
1084
+ </table>
1085
+ </div>
1086
+ </div>
1087
+ </div>
1088
+
1089
+ <!-- Rate Limits Tab -->
1090
+ <div class="tab-content" id="tab-ratelimits">
1091
+ <div class="card">
1092
+ <div class="card-header">
1093
+ <h2 class="card-title">
1094
+ <svg class="icon icon-lg">
1095
+ <circle cx="12" cy="12" r="10"></circle>
1096
+ <polyline points="12 6 12 12 16 14"></polyline>
1097
+ </svg>
1098
+ Rate Limit Monitor
1099
+ </h2>
1100
+ <button class="btn btn-secondary" onclick="loadRateLimits()">
1101
+ <svg class="icon">
1102
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1103
+ </svg>
1104
+ Refresh
1105
+ </button>
1106
+ </div>
1107
+ <div id="rateLimitCards" class="grid grid-2">
1108
+ <div class="loading-inline">
1109
+ <div class="spinner-inline"></div>
1110
+ Loading rate limits...
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+
1116
+ <!-- Logs Tab -->
1117
+ <div class="tab-content" id="tab-logs">
1118
+ <div class="card">
1119
+ <div class="card-header">
1120
+ <h2 class="card-title">
1121
+ <svg class="icon icon-lg">
1122
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
1123
+ <polyline points="14 2 14 8 20 8"></polyline>
1124
+ </svg>
1125
+ Connection Logs
1126
+ </h2>
1127
+ <div class="card-actions">
1128
+ <select id="logType" class="input" style="width: auto; padding: 10px 16px;" onchange="loadLogs()">
1129
+ <option value="connection">Connection</option>
1130
+ <option value="error">Error</option>
1131
+ <option value="rate_limit">Rate Limit</option>
1132
+ <option value="all">All</option>
1133
+ </select>
1134
+ <button class="btn btn-secondary" onclick="loadLogs()">
1135
+ <svg class="icon">
1136
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1137
+ </svg>
1138
+ Refresh
1139
+ </button>
1140
+ </div>
1141
+ </div>
1142
+ <div class="table-container">
1143
+ <table class="table">
1144
+ <thead>
1145
+ <tr>
1146
+ <th>Timestamp</th>
1147
+ <th>Provider</th>
1148
+ <th>Type</th>
1149
+ <th>Status</th>
1150
+ <th>Response Time</th>
1151
+ <th>Message</th>
1152
+ </tr>
1153
+ </thead>
1154
+ <tbody id="logsTableBody">
1155
+ <tr>
1156
+ <td colspan="6">
1157
+ <div class="loading-inline">
1158
+ <div class="spinner-inline"></div>
1159
+ Loading logs...
1160
+ </div>
1161
+ </td>
1162
+ </tr>
1163
+ </tbody>
1164
+ </table>
1165
+ </div>
1166
+ </div>
1167
+ </div>
1168
+
1169
+ <!-- Alerts Tab -->
1170
+ <div class="tab-content" id="tab-alerts">
1171
+ <div class="card">
1172
+ <div class="card-header">
1173
+ <h2 class="card-title">
1174
+ <svg class="icon icon-lg">
1175
+ <circle cx="12" cy="12" r="10"></circle>
1176
+ <line x1="12" y1="8" x2="12" y2="12"></line>
1177
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
1178
+ </svg>
1179
+ System Alerts
1180
+ </h2>
1181
+ <button class="btn btn-secondary" onclick="loadAlerts()">
1182
+ <svg class="icon">
1183
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1184
+ </svg>
1185
+ Refresh
1186
+ </button>
1187
+ </div>
1188
+ <div id="alertsList">
1189
+ <div class="loading-inline">
1190
+ <div class="spinner-inline"></div>
1191
+ Loading alerts...
1192
+ </div>
1193
+ </div>
1194
+ </div>
1195
+ </div>
1196
+
1197
+ <!-- HuggingFace Tab -->
1198
+ <div class="tab-content" id="tab-huggingface">
1199
+ <div class="card">
1200
+ <div class="card-header">
1201
+ <h2 class="card-title">
1202
+ <svg class="icon icon-lg">
1203
+ <circle cx="12" cy="12" r="10"></circle>
1204
+ </svg>
1205
+ 🤗 HuggingFace Health Status
1206
+ </h2>
1207
+ <div class="card-actions">
1208
+ <button class="btn" onclick="refreshHFRegistry()">
1209
+ <svg class="icon">
1210
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1211
+ </svg>
1212
+ Refresh Registry
1213
+ </button>
1214
+ </div>
1215
+ </div>
1216
+ <div id="hfHealthDisplay" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; border: 2px solid var(--border);">
1217
+ Loading HF health status...
1218
+ </div>
1219
+ </div>
1220
+
1221
+ <div class="grid grid-2">
1222
+ <div class="card">
1223
+ <div class="card-header">
1224
+ <h2 class="card-title">
1225
+ Models Registry
1226
+ <span class="badge badge-success" id="hfModelsCount">0</span>
1227
+ </h2>
1228
+ </div>
1229
+ <div id="hfModelsList" style="max-height: 400px; overflow-y: auto;">
1230
+ <div class="loading-inline">
1231
+ <div class="spinner-inline"></div>
1232
+ Loading models...
1233
+ </div>
1234
+ </div>
1235
+ </div>
1236
+
1237
+ <div class="card">
1238
+ <div class="card-header">
1239
+ <h2 class="card-title">
1240
+ Datasets Registry
1241
+ <span class="badge badge-success" id="hfDatasetsCount">0</span>
1242
+ </h2>
1243
+ </div>
1244
+ <div id="hfDatasetsList" style="max-height: 400px; overflow-y: auto;">
1245
+ <div class="loading-inline">
1246
+ <div class="spinner-inline"></div>
1247
+ Loading datasets...
1248
+ </div>
1249
+ </div>
1250
+ </div>
1251
+ </div>
1252
+
1253
+ <div class="card">
1254
+ <div class="card-header">
1255
+ <h2 class="card-title">
1256
+ <svg class="icon icon-lg">
1257
+ <circle cx="11" cy="11" r="8"></circle>
1258
+ <path d="m21 21-4.35-4.35"></path>
1259
+ </svg>
1260
+ Search Registry
1261
+ </h2>
1262
+ </div>
1263
+ <div style="display: flex; gap: 12px; margin-bottom: 20px;">
1264
+ <input type="text" id="hfSearchQuery" placeholder="Search crypto, bitcoin, sentiment..." class="input" style="flex: 1;" value="crypto">
1265
+ <select id="hfSearchKind" class="input" style="width: auto; padding: 12px 16px;">
1266
+ <option value="models">Models</option>
1267
+ <option value="datasets">Datasets</option>
1268
+ </select>
1269
+ <button class="btn" onclick="searchHF()">
1270
+ <svg class="icon">
1271
+ <circle cx="11" cy="11" r="8"></circle>
1272
+ <path d="m21 21-4.35-4.35"></path>
1273
+ </svg>
1274
+ Search
1275
+ </button>
1276
+ </div>
1277
+ <div id="hfSearchResults" style="max-height: 400px; overflow-y: auto; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
1278
+ <div style="text-align: center; color: var(--text-muted);">Enter a query and click search</div>
1279
+ </div>
1280
+ </div>
1281
+
1282
+ <div class="card">
1283
+ <div class="card-header">
1284
+ <h2 class="card-title">💭 Sentiment Analysis</h2>
1285
+ </div>
1286
+ <div style="margin-bottom: 16px;">
1287
+ <label style="display: block; font-weight: 700; margin-bottom: 8px; color: var(--text-primary);">Text Samples (one per line)</label>
1288
+ <textarea id="hfSentimentTexts" rows="6" class="input" placeholder="BTC strong breakout&#10;ETH looks weak&#10;Crypto market is bullish today">BTC strong breakout
1289
+ ETH looks weak
1290
+ Crypto market is bullish today
1291
+ Bears are taking control
1292
+ Neutral market conditions</textarea>
1293
+ </div>
1294
+ <button class="btn" onclick="runHFSentiment()">
1295
+ <svg class="icon">
1296
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
1297
+ </svg>
1298
+ Run Sentiment Analysis
1299
+ </button>
1300
+ <div id="hfSentimentVote" style="margin: 20px 0; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); text-align: center; font-size: 32px; font-weight: 900; border: 2px solid var(--border);">
1301
+ <span style="color: var(--text-muted);">—</span>
1302
+ </div>
1303
+ <div id="hfSentimentResults" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 400px; overflow-y: auto; border: 2px solid var(--border);">
1304
+ Results will appear here...
1305
+ </div>
1306
+ </div>
1307
+ </div>
1308
+ </div>
1309
+
1310
+ <script>
1311
+ // Configuration - آپدیت شده برای Hugging Face Spaces
1312
+ const config = {
1313
+ // استفاده از آدرس نسبی برای Hugging Face
1314
+ apiBaseUrl: '', // خالی بذارید تا از همون origin استفاده کنه
1315
+ wsUrl: (() => {
1316
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1317
+ const host = window.location.host;
1318
+ return `${protocol}//${host}/ws`;
1319
+ })(),
1320
+ autoRefreshInterval: 30000,
1321
+ maxRetries: 3
1322
+ };
1323
+
1324
+ // Global state
1325
+ let state = {
1326
+ ws: null,
1327
+ wsConnected: false,
1328
+ autoRefreshEnabled: true,
1329
+ charts: {},
1330
+ currentTab: 'dashboard',
1331
+ providers: [],
1332
+ categories: [],
1333
+ rateLimits: [],
1334
+ logs: [],
1335
+ alerts: [],
1336
+ lastUpdate: null
1337
+ };
1338
+
1339
+ // Initialize on page load
1340
+ document.addEventListener('DOMContentLoaded', function() {
1341
+ console.log('🚀 Initializing Crypto API Monitor...');
1342
+ console.log('📍 API Base URL:', config.apiBaseUrl);
1343
+ console.log('📡 WebSocket URL:', config.wsUrl);
1344
+
1345
+ discoverEndpoints(); // کشف endpoint های موجود
1346
+ initializeWebSocket();
1347
+ loadInitialData();
1348
+ startAutoRefresh();
1349
+ });
1350
+
1351
+ // تابع برای کشف endpoint های موجود
1352
+ async function discoverEndpoints() {
1353
+ console.log('🔍 Discovering available endpoints...');
1354
+
1355
+ const testEndpoints = [
1356
+ '/',
1357
+ '/health',
1358
+ '/api/health',
1359
+ '/info',
1360
+ '/api/info',
1361
+ '/providers',
1362
+ '/api/providers',
1363
+ '/status',
1364
+ '/api/status',
1365
+ '/api/crypto/market-overview',
1366
+ '/api/crypto/prices/top'
1367
+ ];
1368
+
1369
+ const availableEndpoints = [];
1370
+
1371
+ for (const endpoint of testEndpoints) {
1372
+ try {
1373
+ const response = await fetch(endpoint);
1374
+ console.log(`${endpoint}: ${response.status}`);
1375
+ if (response.ok) {
1376
+ availableEndpoints.push(endpoint);
1377
+ console.log(`✅ Found: ${endpoint}`);
1378
+ }
1379
+ } catch (error) {
1380
+ console.log(`❌ Failed: ${endpoint}`);
1381
+ }
1382
+ }
1383
+
1384
+ console.log('📋 Available endpoints:', availableEndpoints);
1385
+ return availableEndpoints;
1386
+ }
1387
+
1388
+ // WebSocket Connection
1389
+ function initializeWebSocket() {
1390
+ updateWSStatus('connecting');
1391
+
1392
+ const wsEndpoints = [
1393
+ '/ws/live',
1394
+ '/ws',
1395
+ '/live',
1396
+ '/api/ws'
1397
+ ];
1398
+
1399
+ for (const endpoint of wsEndpoints) {
1400
+ try {
1401
+ const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${endpoint}`;
1402
+ console.log(`🔄 Trying WebSocket: ${wsUrl}`);
1403
+
1404
+ state.ws = new WebSocket(wsUrl);
1405
+ setupWebSocketHandlers();
1406
+ break; // اگر موفق بود، بقیه رو امتحان نکن
1407
+ } catch (error) {
1408
+ console.log(`❌ WebSocket failed: ${endpoint}`);
1409
+ }
1410
+ }
1411
+
1412
+ if (!state.ws) {
1413
+ console.log('⚠️ No WebSocket endpoints available');
1414
+ updateWSStatus('disconnected');
1415
+ }
1416
+ }
1417
+
1418
+ function setupWebSocketHandlers() {
1419
+ state.ws.onopen = () => {
1420
+ console.log('✅ WebSocket connected');
1421
+ state.wsConnected = true;
1422
+ updateWSStatus('connected');
1423
+ showToast('Connected', 'Real-time data stream active', 'success');
1424
+ };
1425
+
1426
+ state.ws.onmessage = (event) => {
1427
+ try {
1428
+ const data = JSON.parse(event.data);
1429
+ handleWSMessage(data);
1430
+ } catch (error) {
1431
+ console.error('Error parsing WebSocket message:', error);
1432
+ }
1433
+ };
1434
+
1435
+ state.ws.onerror = (error) => {
1436
+ console.error('❌ WebSocket error:', error);
1437
+ updateWSStatus('disconnected');
1438
+ };
1439
+
1440
+ state.ws.onclose = () => {
1441
+ console.log('⚠️ WebSocket disconnected');
1442
+ state.wsConnected = false;
1443
+ updateWSStatus('disconnected');
1444
+ };
1445
+ }
1446
+
1447
+ function updateWSStatus(status) {
1448
+ const statusEl = document.getElementById('wsStatus');
1449
+ const textEl = document.getElementById('wsStatusText');
1450
+
1451
+ statusEl.classList.remove('connected', 'disconnected', 'connecting');
1452
+ statusEl.classList.add(status);
1453
+
1454
+ const statusText = {
1455
+ 'connected': '✓ Connected',
1456
+ 'disconnected': '✗ Disconnected',
1457
+ 'connecting': '⟳ Connecting...'
1458
+ };
1459
+
1460
+ textEl.textContent = statusText[status] || 'Unknown';
1461
+ }
1462
+
1463
+ function handleWSMessage(data) {
1464
+ console.log('📨 WebSocket message:', data.type);
1465
+
1466
+ switch(data.type) {
1467
+ case 'status_update':
1468
+ updateKPIs(data.data);
1469
+ break;
1470
+ case 'provider_status_change':
1471
+ loadProviders();
1472
+ break;
1473
+ case 'new_alert':
1474
+ addAlert(data.data);
1475
+ break;
1476
+ default:
1477
+ console.log('Unknown message type:', data.type);
1478
+ }
1479
+ }
1480
+
1481
+ // API Calls - آپدیت شده با endpoint های جایگزین
1482
+ async function apiCall(endpoint, options = {}) {
1483
+ try {
1484
+ const url = `${config.apiBaseUrl}${endpoint}`;
1485
+ console.log('🌐 API Call:', url);
1486
+
1487
+ const response = await fetch(url, {
1488
+ ...options,
1489
+ headers: {
1490
+ 'Content-Type': 'application/json',
1491
+ ...options.headers
1492
+ }
1493
+ });
1494
+
1495
+ if (!response.ok) {
1496
+ // اگر 404 باشه، endpoint های جایگزین رو چک کن
1497
+ if (response.status === 404) {
1498
+ console.log(`⚠️ Endpoint ${endpoint} not found, trying alternatives...`);
1499
+ return await tryAlternativeEndpoints(endpoint, options);
1500
+ }
1501
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1502
+ }
1503
+
1504
+ const data = await response.json();
1505
+ console.log('✅ API Response:', endpoint, data);
1506
+ return data;
1507
+ } catch (error) {
1508
+ console.error(`❌ API call failed: ${endpoint}`, error);
1509
+ showToast('API Error', `Failed: ${endpoint}`, 'error');
1510
+ throw error;
1511
+ }
1512
+ }
1513
+
1514
+ // تابع برای امتحان endpoint های جایگزین
1515
+ async function tryAlternativeEndpoints(originalEndpoint, options) {
1516
+ const alternatives = {
1517
+ '/api/providers': ['/providers', '/api/sources', '/status'],
1518
+ '/health': ['/api/health', '/status/health'],
1519
+ '/info': ['/api/info', '/system/info'],
1520
+ '/api/categories': ['/categories', '/api/groups'],
1521
+ '/api/rate-limits': ['/rate-limits', '/api/limits'],
1522
+ '/api/logs': ['/logs', '/api/events'],
1523
+ '/api/alerts': ['/alerts', '/api/notifications'],
1524
+ '/api/hf/health': ['/hf/health', '/api/huggingface/health'],
1525
+ '/api/hf/refresh': ['/hf/refresh', '/api/huggingface/refresh'],
1526
+ '/api/hf/registry': ['/hf/registry', '/api/huggingface/registry'],
1527
+ '/api/hf/search': ['/hf/search', '/api/huggingface/search'],
1528
+ '/api/hf/run-sentiment': ['/hf/sentiment', '/api/huggingface/sentiment']
1529
+ };
1530
+
1531
+ for (const altEndpoint of alternatives[originalEndpoint] || []) {
1532
+ try {
1533
+ const url = `${config.apiBaseUrl}${altEndpoint}`;
1534
+ console.log(`🔄 Trying alternative: ${altEndpoint}`);
1535
+
1536
+ const response = await fetch(url, options);
1537
+ if (response.ok) {
1538
+ const data = await response.json();
1539
+ console.log(`✅ Alternative endpoint worked: ${altEndpoint}`);
1540
+ return data;
1541
+ }
1542
+ } catch (error) {
1543
+ console.log(`❌ Alternative failed: ${altEndpoint}`);
1544
+ }
1545
+ }
1546
+
1547
+ throw new Error(`All endpoints failed for ${originalEndpoint}`);
1548
+ }
1549
+
1550
+ async function loadInitialData() {
1551
+ showLoading();
1552
+
1553
+ try {
1554
+ console.log('📊 Loading initial data...');
1555
+
1556
+ await loadHealth();
1557
+ await loadProviders();
1558
+ await loadSystemInfo();
1559
+
1560
+ initializeCharts();
1561
+
1562
+ state.lastUpdate = new Date();
1563
+ updateLastUpdateDisplay();
1564
+
1565
+ console.log('✅ Initial data loaded successfully');
1566
+ showToast('Success', 'Dashboard loaded successfully', 'success');
1567
+ } catch (error) {
1568
+ console.error('❌ Error loading initial data:', error);
1569
+ showToast('Error', 'Failed to load initial data', 'error');
1570
+ } finally {
1571
+ hideLoading();
1572
+ }
1573
+ }
1574
+
1575
+ async function loadHealth() {
1576
+ try {
1577
+ const data = await apiCall('/health');
1578
+ updateKPIs(data.components || data);
1579
+
1580
+ const statusBadge = document.getElementById('systemStatus');
1581
+ const statusText = document.getElementById('systemStatusText');
1582
+
1583
+ if (data.status === 'healthy') {
1584
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.08))';
1585
+ statusBadge.style.borderColor = 'rgba(16, 185, 129, 0.4)';
1586
+ statusBadge.style.color = 'var(--success)';
1587
+ statusText.textContent = '✓ System Healthy';
1588
+ } else if (data.status === 'degraded') {
1589
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.08))';
1590
+ statusBadge.style.borderColor = 'rgba(245, 158, 11, 0.4)';
1591
+ statusBadge.style.color = 'var(--warning)';
1592
+ statusText.textContent = '⚠ System Degraded';
1593
+ } else {
1594
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.08))';
1595
+ statusBadge.style.borderColor = 'rgba(239, 68, 68, 0.4)';
1596
+ statusBadge.style.color = 'var(--danger)';
1597
+ statusText.textContent = '✗ System Critical';
1598
+ }
1599
+ } catch (error) {
1600
+ console.error('Error loading health:', error);
1601
+ }
1602
+ }
1603
+
1604
+ async function loadSystemInfo() {
1605
+ try {
1606
+ const data = await apiCall('/info');
1607
+ console.log('📋 System Info:', data);
1608
+ } catch (error) {
1609
+ console.error('Error loading system info:', error);
1610
+ }
1611
+ }
1612
+
1613
+ async function loadProviders() {
1614
+ try {
1615
+ const data = await apiCall('/api/providers');
1616
+ state.providers = data;
1617
+ renderProvidersTable(data);
1618
+ renderProvidersDetail(data);
1619
+ updateStatusChart(data);
1620
+ } catch (error) {
1621
+ console.error('Error loading providers:', error);
1622
+ document.getElementById('providersTableBody').innerHTML = `
1623
+ <tr>
1624
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
1625
+ Failed to load providers. Please check if the API endpoint is available.
1626
+ </td>
1627
+ </tr>
1628
+ `;
1629
+ }
1630
+ }
1631
+
1632
+ async function loadCategories() {
1633
+ try {
1634
+ showLoading();
1635
+ const data = await apiCall('/api/categories');
1636
+ state.categories = data;
1637
+ renderCategoriesTable(data);
1638
+ showToast('Success', 'Categories loaded successfully', 'success');
1639
+ } catch (error) {
1640
+ console.error('Error loading categories:', error);
1641
+ showToast('Error', 'Failed to load categories', 'error');
1642
+ } finally {
1643
+ hideLoading();
1644
+ }
1645
+ }
1646
+
1647
+ async function loadRateLimits() {
1648
+ try {
1649
+ showLoading();
1650
+ const data = await apiCall('/api/rate-limits');
1651
+ state.rateLimits = data;
1652
+ renderRateLimitCards(data);
1653
+ showToast('Success', 'Rate limits loaded successfully', 'success');
1654
+ } catch (error) {
1655
+ console.error('Error loading rate limits:', error);
1656
+ showToast('Error', 'Failed to load rate limits', 'error');
1657
+ } finally {
1658
+ hideLoading();
1659
+ }
1660
+ }
1661
+
1662
+ async function loadLogs() {
1663
+ try {
1664
+ showLoading();
1665
+ const logType = document.getElementById('logType').value;
1666
+ const data = await apiCall(`/api/logs?type=${logType}`);
1667
+ state.logs = data;
1668
+ renderLogsTable(data);
1669
+ showToast('Success', 'Logs loaded successfully', 'success');
1670
+ } catch (error) {
1671
+ console.error('Error loading logs:', error);
1672
+ showToast('Error', 'Failed to load logs', 'error');
1673
+ } finally {
1674
+ hideLoading();
1675
+ }
1676
+ }
1677
+
1678
+ async function loadAlerts() {
1679
+ try {
1680
+ showLoading();
1681
+ const data = await apiCall('/api/alerts');
1682
+ state.alerts = data;
1683
+ renderAlertsList(data);
1684
+ showToast('Success', 'Alerts loaded successfully', 'success');
1685
+ } catch (error) {
1686
+ console.error('Error loading alerts:', error);
1687
+ showToast('Error', 'Failed to load alerts', 'error');
1688
+ } finally {
1689
+ hideLoading();
1690
+ }
1691
+ }
1692
+
1693
+ // HuggingFace APIs
1694
+ async function loadHFHealth() {
1695
+ try {
1696
+ const data = await apiCall('/api/hf/health');
1697
+ document.getElementById('hfHealthDisplay').textContent = JSON.stringify(data, null, 2);
1698
+ } catch (error) {
1699
+ console.error('Error loading HF health:', error);
1700
+ document.getElementById('hfHealthDisplay').textContent = 'Error loading health status';
1701
+ }
1702
+ }
1703
+
1704
+ async function refreshHFRegistry() {
1705
+ try {
1706
+ showLoading();
1707
+ const data = await apiCall('/api/hf/refresh', { method: 'POST' });
1708
+ showToast('Success', 'HF Registry refreshed', 'success');
1709
+ loadHFModels();
1710
+ loadHFDatasets();
1711
+ } catch (error) {
1712
+ console.error('Error refreshing HF registry:', error);
1713
+ showToast('Error', 'Failed to refresh registry', 'error');
1714
+ } finally {
1715
+ hideLoading();
1716
+ }
1717
+ }
1718
+
1719
+ async function loadHFModels() {
1720
+ try {
1721
+ const data = await apiCall('/api/hf/registry?type=models');
1722
+ document.getElementById('hfModelsCount').textContent = data.length || 0;
1723
+ document.getElementById('hfModelsList').innerHTML = data.map(item => `
1724
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1725
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1726
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1727
+ </div>
1728
+ `).join('');
1729
+ } catch (error) {
1730
+ console.error('Error loading HF models:', error);
1731
+ document.getElementById('hfModelsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading models</div>';
1732
+ }
1733
+ }
1734
+
1735
+ async function loadHFDatasets() {
1736
+ try {
1737
+ const data = await apiCall('/api/hf/registry?type=datasets');
1738
+ document.getElementById('hfDatasetsCount').textContent = data.length || 0;
1739
+ document.getElementById('hfDatasetsList').innerHTML = data.map(item => `
1740
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1741
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1742
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1743
+ </div>
1744
+ `).join('');
1745
+ } catch (error) {
1746
+ console.error('Error loading HF datasets:', error);
1747
+ document.getElementById('hfDatasetsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading datasets</div>';
1748
+ }
1749
+ }
1750
+
1751
+ async function searchHF() {
1752
+ try {
1753
+ showLoading();
1754
+ const query = document.getElementById('hfSearchQuery').value;
1755
+ const kind = document.getElementById('hfSearchKind').value;
1756
+ const data = await apiCall(`/api/hf/search?q=${encodeURIComponent(query)}&kind=${kind}`);
1757
+
1758
+ document.getElementById('hfSearchResults').innerHTML = data.map(item => `
1759
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1760
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1761
+ <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">${item.description || 'No description'}</div>
1762
+ <div style="font-size: 11px; color: var(--accent-primary);">Downloads: ${item.downloads || 0} • Likes: ${item.likes || 0}</div>
1763
+ </div>
1764
+ `).join('');
1765
+
1766
+ showToast('Success', `Found ${data.length} results`, 'success');
1767
+ } catch (error) {
1768
+ console.error('Error searching HF:', error);
1769
+ showToast('Error', 'Search failed', 'error');
1770
+ } finally {
1771
+ hideLoading();
1772
+ }
1773
+ }
1774
+
1775
+ async function runHFSentiment() {
1776
+ try {
1777
+ showLoading();
1778
+ const texts = document.getElementById('hfSentimentTexts').value.split('\n').filter(t => t.trim());
1779
+
1780
+ const data = await apiCall('/api/hf/run-sentiment', {
1781
+ method: 'POST',
1782
+ body: JSON.stringify({ texts: texts })
1783
+ });
1784
+
1785
+ document.getElementById('hfSentimentResults').textContent = JSON.stringify(data, null, 2);
1786
+
1787
+ // Calculate overall sentiment vote
1788
+ const sentiments = data.results || [];
1789
+ const positive = sentiments.filter(s => s.sentiment === 'positive').length;
1790
+ const negative = sentiments.filter(s => s.sentiment === 'negative').length;
1791
+ const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;
1792
+
1793
+ let overall = 'NEUTRAL';
1794
+ let color = 'var(--info)';
1795
+
1796
+ if (positive > negative && positive > neutral) {
1797
+ overall = 'BULLISH 📈';
1798
+ color = 'var(--success)';
1799
+ } else if (negative > positive && negative > neutral) {
1800
+ overall = 'BEARISH 📉';
1801
+ color = 'var(--danger)';
1802
+ }
1803
+
1804
+ document.getElementById('hfSentimentVote').innerHTML = `
1805
+ <span style="color: ${color};">${overall}</span>
1806
+ <div style="font-size: 14px; margin-top: 8px; color: var(--text-muted);">
1807
+ Positive: ${positive} • Negative: ${negative} • Neutral: ${neutral}
1808
+ </div>
1809
+ `;
1810
+
1811
+ showToast('Success', 'Sentiment analysis completed', 'success');
1812
+ } catch (error) {
1813
+ console.error('Error running sentiment analysis:', error);
1814
+ showToast('Error', 'Sentiment analysis failed', 'error');
1815
+ } finally {
1816
+ hideLoading();
1817
+ }
1818
+ }
1819
+
1820
+ // Rendering Functions
1821
+ function renderProvidersTable(providers) {
1822
+ const tbody = document.getElementById('providersTableBody');
1823
+
1824
+ if (!providers || providers.length === 0) {
1825
+ tbody.innerHTML = `
1826
+ <tr>
1827
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
1828
+ No providers found
1829
+ </td>
1830
+ </tr>
1831
+ `;
1832
+ return;
1833
+ }
1834
+
1835
+ tbody.innerHTML = providers.map(provider => `
1836
+ <tr>
1837
+ <td>
1838
+ <strong>${provider.name || 'Unknown'}</strong>
1839
+ <div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
1840
+ </td>
1841
+ <td>${provider.category || 'General'}</td>
1842
+ <td>
1843
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1844
+ ${provider.status || 'unknown'}
1845
+ </span>
1846
+ </td>
1847
+ <td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
1848
+ <td>
1849
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(provider.last_checked)}</div>
1850
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(provider.last_checked)}</div>
1851
+ </td>
1852
+ </tr>
1853
+ `).join('');
1854
+ }
1855
+
1856
+ function renderProvidersDetail(providers) {
1857
+ const container = document.getElementById('providersDetail');
1858
+
1859
+ if (!providers || providers.length === 0) {
1860
+ container.innerHTML = `
1861
+ <div style="text-align: center; color: var(--text-muted); padding: 40px;">
1862
+ No providers data available
1863
+ </div>
1864
+ `;
1865
+ return;
1866
+ }
1867
+
1868
+ container.innerHTML = `
1869
+ <div class="table-container">
1870
+ <table class="table">
1871
+ <thead>
1872
+ <tr>
1873
+ <th>Provider</th>
1874
+ <th>Status</th>
1875
+ <th>Response Time</th>
1876
+ <th>Success Rate</th>
1877
+ <th>Last Success</th>
1878
+ <th>Errors (24h)</th>
1879
+ </tr>
1880
+ </thead>
1881
+ <tbody>
1882
+ ${providers.map(provider => `
1883
+ <tr>
1884
+ <td>
1885
+ <strong>${provider.name || 'Unknown'}</strong>
1886
+ <div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
1887
+ </td>
1888
+ <td>
1889
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1890
+ ${provider.status || 'unknown'}
1891
+ </span>
1892
+ </td>
1893
+ <td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
1894
+ <td>
1895
+ <div class="progress">
1896
+ <div class="progress-bar ${getHealthClass(provider.success_rate || 0)}"
1897
+ style="width: ${provider.success_rate || 0}%"></div>
1898
+ </div>
1899
+ <small>${Math.round(provider.success_rate || 0)}%</small>
1900
+ </td>
1901
+ <td>${formatTimestamp(provider.last_success)}</td>
1902
+ <td>${provider.error_count_24h || 0}</td>
1903
+ </tr>
1904
+ `).join('')}
1905
+ </tbody>
1906
+ </table>
1907
+ </div>
1908
+ `;
1909
+ }
1910
+
1911
+ function renderCategoriesTable(categories) {
1912
+ const tbody = document.getElementById('categoriesTableBody');
1913
+
1914
+ if (!categories || categories.length === 0) {
1915
+ tbody.innerHTML = `
1916
+ <tr>
1917
+ <td colspan="7" style="text-align: center; color: var(--text-muted); padding: 40px;">
1918
+ No categories found
1919
+ </td>
1920
+ </tr>
1921
+ `;
1922
+ return;
1923
+ }
1924
+
1925
+ tbody.innerHTML = categories.map(category => `
1926
+ <tr>
1927
+ <td>
1928
+ <strong>${category.name || 'Unnamed'}</strong>
1929
+ </td>
1930
+ <td>${category.total_sources || 0}</td>
1931
+ <td>${category.online || 0}</td>
1932
+ <td>
1933
+ <div class="progress">
1934
+ <div class="progress-bar ${getHealthClass(category.health_percentage || 0)}"
1935
+ style="width: ${category.health_percentage || 0}%"></div>
1936
+ </div>
1937
+ <small>${Math.round(category.health_percentage || 0)}%</small>
1938
+ </td>
1939
+ <td>${category.avg_response || '--'}ms</td>
1940
+ <td>${formatTimestamp(category.last_updated)}</td>
1941
+ <td>
1942
+ <span class="badge ${getStatusBadgeClass(category.status)}">
1943
+ ${category.status || 'unknown'}
1944
+ </span>
1945
+ </td>
1946
+ </tr>
1947
+ `).join('');
1948
+ }
1949
+
1950
+ function renderRateLimitCards(rateLimits) {
1951
+ const container = document.getElementById('rateLimitCards');
1952
+
1953
+ if (!rateLimits || rateLimits.length === 0) {
1954
+ container.innerHTML = `
1955
+ <div class="card" style="grid-column: 1 / -1; text-align: center; padding: 40px;">
1956
+ <div style="color: var(--text-muted); font-size: 16px;">
1957
+ No rate limit data available
1958
+ </div>
1959
+ </div>
1960
+ `;
1961
+ return;
1962
+ }
1963
+
1964
+ container.innerHTML = rateLimits.map(limit => `
1965
+ <div class="card">
1966
+ <div class="card-header">
1967
+ <h3 class="card-title" style="font-size: 16px;">
1968
+ ${limit.provider || 'Unknown Provider'}
1969
+ </h3>
1970
+ <span class="badge ${getRateLimitStatusClass(limit)}">
1971
+ ${getRateLimitStatus(limit)}
1972
+ </span>
1973
+ </div>
1974
+
1975
+ <div style="margin-bottom: 16px;">
1976
+ <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
1977
+ <span style="font-size: 12px; color: var(--text-muted);">Usage</span>
1978
+ <span style="font-size: 12px; font-weight: 700;">
1979
+ ${limit.used || 0}/${limit.limit || 0}
1980
+ </span>
1981
+ </div>
1982
+ <div class="progress">
1983
+ <div class="progress-bar ${getRateLimitProgressClass(limit)}"
1984
+ style="width: ${calculateUsagePercentage(limit)}%"></div>
1985
+ </div>
1986
+ </div>
1987
+
1988
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 12px;">
1989
+ <div>
1990
+ <div style="color: var(--text-muted);">Reset In</div>
1991
+ <div style="font-weight: 700;">${formatResetTime(limit.reset_time)}</div>
1992
+ </div>
1993
+ <div>
1994
+ <div style="color: var(--text-muted);">Window</div>
1995
+ <div style="font-weight: 700;">${limit.window || 'N/A'}</div>
1996
+ </div>
1997
+ </div>
1998
+
1999
+ ${limit.endpoint ? `
2000
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
2001
+ <div style="font-size: 11px; color: var(--text-muted);">Endpoint</div>
2002
+ <div style="font-size: 12px; font-family: monospace;">${limit.endpoint}</div>
2003
+ </div>
2004
+ ` : ''}
2005
+ </div>
2006
+ `).join('');
2007
+ }
2008
+
2009
+ function renderLogsTable(logs) {
2010
+ const tbody = document.getElementById('logsTableBody');
2011
+
2012
+ if (!logs || logs.length === 0) {
2013
+ tbody.innerHTML = `
2014
+ <tr>
2015
+ <td colspan="6" style="text-align: center; color: var(--text-muted); padding: 40px;">
2016
+ No logs found
2017
+ </td>
2018
+ </tr>
2019
+ `;
2020
+ return;
2021
+ }
2022
+
2023
+ tbody.innerHTML = logs.map(log => `
2024
+ <tr>
2025
+ <td>
2026
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(log.timestamp)}</div>
2027
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(log.timestamp)}</div>
2028
+ </td>
2029
+ <td>
2030
+ <strong>${log.provider || 'System'}</strong>
2031
+ </td>
2032
+ <td>
2033
+ <span class="badge ${getLogTypeClass(log.type)}">
2034
+ ${log.type || 'unknown'}
2035
+ </span>
2036
+ </td>
2037
+ <td>
2038
+ <span class="badge ${getStatusBadgeClass(log.status)}">
2039
+ ${log.status || 'unknown'}
2040
+ </span>
2041
+ </td>
2042
+ <td>${log.response_time ? log.response_time + 'ms' : '--'}</td>
2043
+ <td>
2044
+ <div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
2045
+ ${log.message || 'No message'}
2046
+ </div>
2047
+ </td>
2048
+ </tr>
2049
+ `).join('');
2050
+ }
2051
+
2052
+ function renderAlertsList(alerts) {
2053
+ const container = document.getElementById('alertsList');
2054
+
2055
+ if (!alerts || alerts.length === 0) {
2056
+ container.innerHTML = `
2057
+ <div class="alert alert-success">
2058
+ <div class="alert-content">
2059
+ <div class="alert-title">No Active Alerts</div>
2060
+ <div class="alert-message">All systems are operating normally</div>
2061
+ </div>
2062
+ </div>
2063
+ `;
2064
+ return;
2065
+ }
2066
+
2067
+ container.innerHTML = alerts.map(alert => `
2068
+ <div class="alert ${getAlertClass(alert.severity)}">
2069
+ <div class="alert-content">
2070
+ <div class="alert-title">
2071
+ ${alert.title || 'Alert'}
2072
+ <span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
2073
+ ${formatTimeAgo(alert.timestamp)}
2074
+ </span>
2075
+ </div>
2076
+ <div class="alert-message">
2077
+ ${alert.message || 'No message provided'}
2078
+ </div>
2079
+ ${alert.provider ? `
2080
+ <div style="margin-top: 8px; font-size: 12px;">
2081
+ <strong>Provider:</strong> ${alert.provider}
2082
+ </div>
2083
+ ` : ''}
2084
+ </div>
2085
+ </div>
2086
+ `).join('');
2087
+ }
2088
+
2089
+ // Helper functions
2090
+ function getHealthClass(percentage) {
2091
+ if (percentage >= 80) return 'success';
2092
+ if (percentage >= 60) return 'warning';
2093
+ return 'danger';
2094
+ }
2095
+
2096
+ function getStatusBadgeClass(status) {
2097
+ switch (status?.toLowerCase()) {
2098
+ case 'healthy': case 'online': case 'success': return 'badge-success';
2099
+ case 'degraded': case 'warning': return 'badge-warning';
2100
+ case 'offline': case 'error': case 'critical': return 'badge-danger';
2101
+ default: return 'badge-info';
2102
+ }
2103
+ }
2104
+
2105
+ function getLogTypeClass(type) {
2106
+ switch (type?.toLowerCase()) {
2107
+ case 'error': return 'badge-danger';
2108
+ case 'warning': return 'badge-warning';
2109
+ case 'info': case 'connection': return 'badge-info';
2110
+ case 'success': return 'badge-success';
2111
+ default: return 'badge-info';
2112
+ }
2113
+ }
2114
+
2115
+ function getAlertClass(severity) {
2116
+ switch (severity?.toLowerCase()) {
2117
+ case 'critical': case 'error': return 'alert-danger';
2118
+ case 'warning': return 'alert-warning';
2119
+ case 'info': return 'alert-info';
2120
+ case 'success': return 'alert-success';
2121
+ default: return 'alert-info';
2122
+ }
2123
+ }
2124
+
2125
+ function getRateLimitStatusClass(limit) {
2126
+ const usage = calculateUsagePercentage(limit);
2127
+ if (usage >= 90) return 'badge-danger';
2128
+ if (usage >= 75) return 'badge-warning';
2129
+ return 'badge-success';
2130
+ }
2131
+
2132
+ function getRateLimitStatus(limit) {
2133
+ const usage = calculateUsagePercentage(limit);
2134
+ if (usage >= 90) return 'Critical';
2135
+ if (usage >= 75) return 'Warning';
2136
+ return 'Normal';
2137
+ }
2138
+
2139
+ function getRateLimitProgressClass(limit) {
2140
+ const usage = calculateUsagePercentage(limit);
2141
+ if (usage >= 90) return 'danger';
2142
+ if (usage >= 75) return 'warning';
2143
+ return 'success';
2144
+ }
2145
+
2146
+ function calculateUsagePercentage(limit) {
2147
+ if (!limit.limit || limit.limit === 0) return 0;
2148
+ return Math.min(100, ((limit.used || 0) / limit.limit) * 100);
2149
+ }
2150
+
2151
+ function formatResetTime(resetTime) {
2152
+ if (!resetTime) return 'N/A';
2153
+ // Simple formatting - you can enhance this with proper time parsing
2154
+ return typeof resetTime === 'string' ? resetTime : 'Soon';
2155
+ }
2156
+
2157
+ function formatTimestamp(timestamp) {
2158
+ if (!timestamp) return '--';
2159
+ try {
2160
+ return new Date(timestamp).toLocaleString();
2161
+ } catch {
2162
+ return 'Invalid Date';
2163
+ }
2164
+ }
2165
+
2166
+ function formatTimeAgo(timestamp) {
2167
+ if (!timestamp) return '';
2168
+ try {
2169
+ const now = new Date();
2170
+ const time = new Date(timestamp);
2171
+ const diff = now - time;
2172
+
2173
+ const minutes = Math.floor(diff / 60000);
2174
+ const hours = Math.floor(diff / 3600000);
2175
+ const days = Math.floor(diff / 86400000);
2176
+
2177
+ if (days > 0) return `${days}d ago`;
2178
+ if (hours > 0) return `${hours}h ago`;
2179
+ if (minutes > 0) return `${minutes}m ago`;
2180
+ return 'Just now';
2181
+ } catch {
2182
+ return 'Unknown';
2183
+ }
2184
+ }
2185
+
2186
+ // KPI Updates
2187
+ function updateKPIs(data) {
2188
+ if (!data) return;
2189
+
2190
+ // Update Total APIs
2191
+ const totalAPIs = data.length || 0;
2192
+ document.getElementById('kpiTotalAPIs').textContent = totalAPIs;
2193
+ document.getElementById('kpiTotalTrend').textContent = `${totalAPIs} active`;
2194
+
2195
+ // Update Online count
2196
+ const onlineCount = data.filter(p => p.status === 'online' || p.status === 'healthy').length;
2197
+ document.getElementById('kpiOnline').textContent = onlineCount;
2198
+ document.getElementById('kpiOnlineTrend').textContent = `${Math.round((onlineCount / totalAPIs) * 100)}% uptime`;
2199
+
2200
+ // Update Average Response
2201
+ const validResponses = data.filter(p => p.response_time).map(p => p.response_time);
2202
+ const avgResponse = validResponses.length > 0 ?
2203
+ Math.round(validResponses.reduce((a, b) => a + b, 0) / validResponses.length) : 0;
2204
+
2205
+ document.getElementById('kpiAvgResponse').textContent = avgResponse + 'ms';
2206
+
2207
+ const responseTrend = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
2208
+ document.getElementById('kpiResponseTrend').textContent = responseTrend;
2209
+
2210
+ const trendElement = document.querySelector('#kpiAvgResponse').nextElementSibling;
2211
+ trendElement.className = `kpi-trend ${
2212
+ avgResponse < 500 ? 'trend-up' : avgResponse < 1000 ? 'trend-neutral' : 'trend-down'
2213
+ }`;
2214
+ }
2215
+
2216
+ function updateLastUpdateDisplay() {
2217
+ if (state.lastUpdate) {
2218
+ document.getElementById('kpiLastUpdate').textContent =
2219
+ state.lastUpdate.toLocaleTimeString() + '\n' + state.lastUpdate.toLocaleDateString();
2220
+ }
2221
+ }
2222
+
2223
+ // Chart Functions
2224
+ function initializeCharts() {
2225
+ // Health Chart
2226
+ const healthCtx = document.getElementById('healthChart').getContext('2d');
2227
+ state.charts.health = new Chart(healthCtx, {
2228
+ type: 'line',
2229
+ data: {
2230
+ labels: [],
2231
+ datasets: [{
2232
+ label: 'System Health %',
2233
+ data: [],
2234
+ borderColor: '#8b5cf6',
2235
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
2236
+ borderWidth: 3,
2237
+ fill: true,
2238
+ tension: 0.4
2239
+ }]
2240
+ },
2241
+ options: {
2242
+ responsive: true,
2243
+ maintainAspectRatio: false,
2244
+ plugins: {
2245
+ legend: {
2246
+ display: false
2247
+ }
2248
+ },
2249
+ scales: {
2250
+ y: {
2251
+ beginAtZero: true,
2252
+ max: 100,
2253
+ grid: {
2254
+ color: 'rgba(139, 92, 246, 0.1)'
2255
+ }
2256
+ },
2257
+ x: {
2258
+ grid: {
2259
+ color: 'rgba(139, 92, 246, 0.1)'
2260
+ }
2261
+ }
2262
+ }
2263
+ }
2264
+ });
2265
+
2266
+ // Status Chart
2267
+ const statusCtx = document.getElementById('statusChart').getContext('2d');
2268
+ state.charts.status = new Chart(statusCtx, {
2269
+ type: 'doughnut',
2270
+ data: {
2271
+ labels: ['Online', 'Degraded', 'Offline'],
2272
+ datasets: [{
2273
+ data: [0, 0, 0],
2274
+ backgroundColor: [
2275
+ '#10b981',
2276
+ '#f59e0b',
2277
+ '#ef4444'
2278
+ ],
2279
+ borderWidth: 0
2280
+ }]
2281
+ },
2282
+ options: {
2283
+ responsive: true,
2284
+ maintainAspectRatio: false,
2285
+ cutout: '70%',
2286
+ plugins: {
2287
+ legend: {
2288
+ position: 'bottom'
2289
+ }
2290
+ }
2291
+ }
2292
+ });
2293
+ }
2294
+
2295
+ function updateStatusChart(providers) {
2296
+ if (!state.charts.status || !providers) return;
2297
+
2298
+ const online = providers.filter(p => p.status === 'online' || p.status === 'healthy').length;
2299
+ const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'warning').length;
2300
+ const offline = providers.filter(p => p.status === 'offline' || p.status === 'error').length;
2301
+
2302
+ state.charts.status.data.datasets[0].data = [online, degraded, offline];
2303
+ state.charts.status.update();
2304
+ }
2305
+
2306
+ // Tab Management
2307
+ function switchTab(event, tabName) {
2308
+ // Remove active class from all tabs
2309
+ document.querySelectorAll('.tab').forEach(tab => {
2310
+ tab.classList.remove('active');
2311
+ });
2312
+
2313
+ // Remove active class from all contents
2314
+ document.querySelectorAll('.tab-content').forEach(content => {
2315
+ content.classList.remove('active');
2316
+ });
2317
+
2318
+ // Add active class to clicked tab
2319
+ event.currentTarget.classList.add('active');
2320
+
2321
+ // Show corresponding content
2322
+ document.getElementById(`tab-${tabName}`).classList.add('active');
2323
+
2324
+ // Load data for the tab
2325
+ switch(tabName) {
2326
+ case 'dashboard':
2327
+ loadProviders();
2328
+ break;
2329
+ case 'providers':
2330
+ loadProviders();
2331
+ break;
2332
+ case 'categories':
2333
+ loadCategories();
2334
+ break;
2335
+ case 'ratelimits':
2336
+ loadRateLimits();
2337
+ break;
2338
+ case 'logs':
2339
+ loadLogs();
2340
+ break;
2341
+ case 'alerts':
2342
+ loadAlerts();
2343
+ break;
2344
+ case 'huggingface':
2345
+ loadHFHealth();
2346
+ loadHFModels();
2347
+ loadHFDatasets();
2348
+ break;
2349
+ }
2350
+
2351
+ state.currentTab = tabName;
2352
+ }
2353
+
2354
+ // Utility Functions
2355
+ function showLoading() {
2356
+ document.getElementById('loadingOverlay').classList.add('active');
2357
+ }
2358
+
2359
+ function hideLoading() {
2360
+ document.getElementById('loadingOverlay').classList.remove('active');
2361
+ }
2362
+
2363
+ function showToast(title, message, type = 'info') {
2364
+ const container = document.getElementById('toastContainer');
2365
+ const toast = document.createElement('div');
2366
+ toast.className = `toast ${type}`;
2367
+ toast.innerHTML = `
2368
+ <div class="toast-content">
2369
+ <div class="toast-title">${title}</div>
2370
+ <div class="toast-message">${message}</div>
2371
+ </div>
2372
+ <button onclick="this.parentElement.remove()" style="background: none; border: none; cursor: pointer; color: inherit;">
2373
+ <svg class="icon" style="width: 16px; height: 16px;">
2374
+ <line x1="18" y1="6" x2="6" y2="18"></line>
2375
+ <line x1="6" y1="6" x2="18" y2="18"></line>
2376
+ </svg>
2377
+ </button>
2378
+ `;
2379
+
2380
+ container.appendChild(toast);
2381
+
2382
+ // Auto remove after 5 seconds
2383
+ setTimeout(() => {
2384
+ if (toast.parentElement) {
2385
+ toast.remove();
2386
+ }
2387
+ }, 5000);
2388
+ }
2389
+
2390
+ function addAlert(alert) {
2391
+ const container = document.getElementById('alertsContainer');
2392
+ const alertEl = document.createElement('div');
2393
+ alertEl.className = `alert ${getAlertClass(alert.severity)}`;
2394
+ alertEl.innerHTML = `
2395
+ <div class="alert-content">
2396
+ <div class="alert-title">
2397
+ ${alert.title}
2398
+ <span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
2399
+ ${formatTimeAgo(alert.timestamp)}
2400
+ </span>
2401
+ </div>
2402
+ <div class="alert-message">${alert.message}</div>
2403
+ </div>
2404
+ `;
2405
+
2406
+ container.appendChild(alertEl);
2407
+
2408
+ // Auto remove after 10 seconds
2409
+ setTimeout(() => {
2410
+ if (alertEl.parentElement) {
2411
+ alertEl.remove();
2412
+ }
2413
+ }, 10000);
2414
+ }
2415
+
2416
+ function refreshAll() {
2417
+ console.log('🔄 Refreshing all data...');
2418
+ loadInitialData();
2419
+
2420
+ // Refresh current tab data
2421
+ switch(state.currentTab) {
2422
+ case 'categories':
2423
+ loadCategories();
2424
+ break;
2425
+ case 'ratelimits':
2426
+ loadRateLimits();
2427
+ break;
2428
+ case 'logs':
2429
+ loadLogs();
2430
+ break;
2431
+ case 'alerts':
2432
+ loadAlerts();
2433
+ break;
2434
+ case 'huggingface':
2435
+ loadHFHealth();
2436
+ loadHFModels();
2437
+ loadHFDatasets();
2438
+ break;
2439
+ }
2440
+ }
2441
+
2442
+ function startAutoRefresh() {
2443
+ setInterval(() => {
2444
+ if (state.autoRefreshEnabled && state.wsConnected) {
2445
+ console.log('🔄 Auto-refreshing data...');
2446
+ refreshAll();
2447
+ }
2448
+ }, config.autoRefreshInterval);
2449
+ }
2450
+ </script>
2451
+ </body>
2452
+ </html>
archive_html/index_enhanced.html ADDED
@@ -0,0 +1,2132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 - Professional Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&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
+ :root {
11
+ --bg-primary: #0f0f23;
12
+ --bg-secondary: #1a1a2e;
13
+ --bg-card: #16213e;
14
+ --bg-hover: #1f2b4d;
15
+ --text-primary: #ffffff;
16
+ --text-secondary: #b8c1ec;
17
+ --text-muted: #8892b0;
18
+
19
+ --accent-blue: #00d4ff;
20
+ --accent-purple: #a855f7;
21
+ --accent-pink: #ec4899;
22
+ --accent-green: #10b981;
23
+ --accent-yellow: #fbbf24;
24
+ --accent-red: #ef4444;
25
+ --accent-orange: #f97316;
26
+ --accent-cyan: #06b6d4;
27
+
28
+ --success: #10b981;
29
+ --success-glow: rgba(16, 185, 129, 0.3);
30
+ --warning: #fbbf24;
31
+ --warning-glow: rgba(251, 191, 36, 0.3);
32
+ --danger: #ef4444;
33
+ --danger-glow: rgba(239, 68, 68, 0.3);
34
+ --info: #00d4ff;
35
+ --info-glow: rgba(0, 212, 255, 0.3);
36
+
37
+ --border: rgba(255, 255, 255, 0.1);
38
+ --shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
39
+ --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.6);
40
+ --glow: 0 0 20px;
41
+
42
+ --radius: 16px;
43
+ --radius-lg: 24px;
44
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
45
+ }
46
+
47
+ * {
48
+ margin: 0;
49
+ padding: 0;
50
+ box-sizing: border-box;
51
+ }
52
+
53
+ body {
54
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
55
+ background: var(--bg-primary);
56
+ color: var(--text-primary);
57
+ line-height: 1.6;
58
+ min-height: 100vh;
59
+ overflow-x: hidden;
60
+ }
61
+
62
+ /* Animated Background */
63
+ body::before {
64
+ content: '';
65
+ position: fixed;
66
+ top: 0;
67
+ left: 0;
68
+ right: 0;
69
+ bottom: 0;
70
+ background:
71
+ radial-gradient(circle at 20% 30%, rgba(168, 85, 247, 0.15) 0%, transparent 50%),
72
+ radial-gradient(circle at 80% 70%, rgba(0, 212, 255, 0.15) 0%, transparent 50%),
73
+ radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.1) 0%, transparent 70%);
74
+ pointer-events: none;
75
+ z-index: 0;
76
+ animation: backgroundPulse 20s ease-in-out infinite;
77
+ }
78
+
79
+ @keyframes backgroundPulse {
80
+ 0%, 100% { opacity: 0.5; }
81
+ 50% { opacity: 0.8; }
82
+ }
83
+
84
+ .container {
85
+ max-width: 1800px;
86
+ margin: 0 auto;
87
+ padding: 32px;
88
+ position: relative;
89
+ z-index: 1;
90
+ }
91
+
92
+ /* Header */
93
+ .header {
94
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
95
+ border: 2px solid var(--border);
96
+ border-radius: var(--radius-lg);
97
+ padding: 40px;
98
+ margin-bottom: 32px;
99
+ box-shadow: var(--shadow-lg);
100
+ position: relative;
101
+ overflow: hidden;
102
+ }
103
+
104
+ .header::before {
105
+ content: '';
106
+ position: absolute;
107
+ top: 0;
108
+ left: 0;
109
+ right: 0;
110
+ height: 4px;
111
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
112
+ animation: gradientSlide 3s linear infinite;
113
+ }
114
+
115
+ @keyframes gradientSlide {
116
+ 0% { transform: translateX(-100%); }
117
+ 100% { transform: translateX(100%); }
118
+ }
119
+
120
+ .header-top {
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: space-between;
124
+ flex-wrap: wrap;
125
+ gap: 24px;
126
+ margin-bottom: 32px;
127
+ }
128
+
129
+ .logo {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 20px;
133
+ }
134
+
135
+ .logo-icon {
136
+ width: 80px;
137
+ height: 80px;
138
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
139
+ border-radius: 24px;
140
+ display: flex;
141
+ align-items: center;
142
+ justify-content: center;
143
+ box-shadow: var(--glow) var(--info-glow);
144
+ position: relative;
145
+ overflow: hidden;
146
+ animation: logoFloat 3s ease-in-out infinite;
147
+ }
148
+
149
+ @keyframes logoFloat {
150
+ 0%, 100% { transform: translateY(0px) rotate(0deg); }
151
+ 50% { transform: translateY(-10px) rotate(5deg); }
152
+ }
153
+
154
+ .logo-icon::after {
155
+ content: '';
156
+ position: absolute;
157
+ top: -50%;
158
+ left: -50%;
159
+ right: -50%;
160
+ bottom: -50%;
161
+ background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.4), transparent);
162
+ animation: shimmer 2s infinite;
163
+ }
164
+
165
+ @keyframes shimmer {
166
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
167
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
168
+ }
169
+
170
+ .logo-text h1 {
171
+ font-size: 36px;
172
+ font-weight: 900;
173
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
174
+ -webkit-background-clip: text;
175
+ -webkit-text-fill-color: transparent;
176
+ background-clip: text;
177
+ margin-bottom: 8px;
178
+ letter-spacing: -1px;
179
+ text-shadow: 0 0 30px var(--info-glow);
180
+ }
181
+
182
+ .logo-text p {
183
+ font-size: 14px;
184
+ color: var(--text-secondary);
185
+ font-weight: 600;
186
+ letter-spacing: 0.5px;
187
+ }
188
+
189
+ .header-actions {
190
+ display: flex;
191
+ gap: 16px;
192
+ align-items: center;
193
+ flex-wrap: wrap;
194
+ }
195
+
196
+ .status-badge {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 12px;
200
+ padding: 14px 24px;
201
+ border-radius: 999px;
202
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(16, 185, 129, 0.1) 100%);
203
+ border: 2px solid var(--success);
204
+ font-size: 14px;
205
+ font-weight: 700;
206
+ color: var(--success);
207
+ box-shadow: var(--glow) var(--success-glow);
208
+ text-transform: uppercase;
209
+ letter-spacing: 1px;
210
+ }
211
+
212
+ .status-dot {
213
+ width: 12px;
214
+ height: 12px;
215
+ border-radius: 50%;
216
+ background: var(--success);
217
+ animation: pulse-glow 2s infinite;
218
+ }
219
+
220
+ @keyframes pulse-glow {
221
+ 0%, 100% {
222
+ box-shadow: 0 0 0 0 var(--success-glow),
223
+ 0 0 15px var(--success);
224
+ }
225
+ 50% {
226
+ box-shadow: 0 0 0 10px transparent,
227
+ 0 0 25px var(--success);
228
+ }
229
+ }
230
+
231
+ .connection-status {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 10px;
235
+ padding: 12px 20px;
236
+ border-radius: 999px;
237
+ background: var(--bg-secondary);
238
+ border: 2px solid var(--border);
239
+ font-size: 13px;
240
+ font-weight: 700;
241
+ transition: var(--transition);
242
+ }
243
+
244
+ .connection-status.connected {
245
+ border-color: var(--success);
246
+ color: var(--success);
247
+ box-shadow: var(--glow) var(--success-glow);
248
+ }
249
+ .connection-status.disconnected {
250
+ border-color: var(--danger);
251
+ color: var(--danger);
252
+ box-shadow: var(--glow) var(--danger-glow);
253
+ }
254
+ .connection-status.connecting {
255
+ border-color: var(--warning);
256
+ color: var(--warning);
257
+ box-shadow: var(--glow) var(--warning-glow);
258
+ }
259
+
260
+ .btn {
261
+ padding: 14px 28px;
262
+ border-radius: 14px;
263
+ border: none;
264
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
265
+ color: white;
266
+ font-family: inherit;
267
+ font-size: 14px;
268
+ font-weight: 700;
269
+ cursor: pointer;
270
+ transition: var(--transition);
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 10px;
274
+ box-shadow: var(--glow) var(--info-glow);
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.5px;
277
+ position: relative;
278
+ overflow: hidden;
279
+ }
280
+
281
+ .btn::before {
282
+ content: '';
283
+ position: absolute;
284
+ top: 0;
285
+ left: -100%;
286
+ width: 100%;
287
+ height: 100%;
288
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
289
+ transition: left 0.5s;
290
+ }
291
+
292
+ .btn:hover {
293
+ transform: translateY(-3px) scale(1.05);
294
+ box-shadow: 0 15px 40px var(--info-glow);
295
+ }
296
+
297
+ .btn:hover::before {
298
+ left: 100%;
299
+ }
300
+
301
+ .btn:active {
302
+ transform: translateY(-1px) scale(1.02);
303
+ }
304
+
305
+ /* KPI Cards */
306
+ .kpi-grid {
307
+ display: grid;
308
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
309
+ gap: 24px;
310
+ margin-bottom: 32px;
311
+ }
312
+
313
+ .kpi-card {
314
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
315
+ border: 2px solid var(--border);
316
+ border-radius: var(--radius-lg);
317
+ padding: 32px;
318
+ transition: var(--transition);
319
+ box-shadow: var(--shadow);
320
+ position: relative;
321
+ overflow: hidden;
322
+ cursor: pointer;
323
+ }
324
+
325
+ .kpi-card::before {
326
+ content: '';
327
+ position: absolute;
328
+ top: 0;
329
+ left: 0;
330
+ right: 0;
331
+ height: 4px;
332
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
333
+ transform: scaleX(0);
334
+ transform-origin: left;
335
+ transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
336
+ }
337
+
338
+ .kpi-card:hover {
339
+ transform: translateY(-12px) scale(1.03);
340
+ box-shadow: 0 20px 60px rgba(0, 212, 255, 0.3);
341
+ border-color: var(--accent-blue);
342
+ }
343
+
344
+ .kpi-card:hover::before {
345
+ transform: scaleX(1);
346
+ }
347
+
348
+ .kpi-header {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: space-between;
352
+ margin-bottom: 24px;
353
+ }
354
+
355
+ .kpi-label {
356
+ font-size: 12px;
357
+ color: var(--text-muted);
358
+ font-weight: 800;
359
+ text-transform: uppercase;
360
+ letter-spacing: 1.5px;
361
+ }
362
+
363
+ .kpi-icon-wrapper {
364
+ width: 72px;
365
+ height: 72px;
366
+ border-radius: 20px;
367
+ display: flex;
368
+ align-items: center;
369
+ justify-content: center;
370
+ transition: var(--transition);
371
+ box-shadow: var(--shadow);
372
+ }
373
+
374
+ .kpi-card:hover .kpi-icon-wrapper {
375
+ transform: rotate(-10deg) scale(1.2);
376
+ box-shadow: 0 15px 40px rgba(0, 212, 255, 0.4);
377
+ }
378
+
379
+ .kpi-value {
380
+ font-size: 56px;
381
+ font-weight: 900;
382
+ margin-bottom: 20px;
383
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
384
+ -webkit-background-clip: text;
385
+ -webkit-text-fill-color: transparent;
386
+ background-clip: text;
387
+ line-height: 1;
388
+ animation: countUp 0.6s ease-out;
389
+ letter-spacing: -3px;
390
+ }
391
+
392
+ @keyframes countUp {
393
+ from { opacity: 0; transform: translateY(30px); }
394
+ to { opacity: 1; transform: translateY(0); }
395
+ }
396
+
397
+ .kpi-trend {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 10px;
401
+ font-size: 13px;
402
+ font-weight: 700;
403
+ padding: 10px 18px;
404
+ border-radius: 12px;
405
+ width: fit-content;
406
+ text-transform: uppercase;
407
+ letter-spacing: 0.5px;
408
+ }
409
+
410
+ .trend-up {
411
+ color: var(--success);
412
+ background: rgba(16, 185, 129, 0.15);
413
+ border: 2px solid var(--success);
414
+ box-shadow: var(--glow) var(--success-glow);
415
+ }
416
+
417
+ .trend-down {
418
+ color: var(--danger);
419
+ background: rgba(239, 68, 68, 0.15);
420
+ border: 2px solid var(--danger);
421
+ box-shadow: var(--glow) var(--danger-glow);
422
+ }
423
+
424
+ .trend-neutral {
425
+ color: var(--info);
426
+ background: rgba(0, 212, 255, 0.15);
427
+ border: 2px solid var(--info);
428
+ box-shadow: var(--glow) var(--info-glow);
429
+ }
430
+
431
+ /* Tabs */
432
+ .tabs {
433
+ display: flex;
434
+ gap: 8px;
435
+ margin-bottom: 32px;
436
+ overflow-x: auto;
437
+ padding: 12px;
438
+ background: var(--bg-card);
439
+ border-radius: var(--radius-lg);
440
+ border: 2px solid var(--border);
441
+ box-shadow: var(--shadow);
442
+ }
443
+
444
+ .tab {
445
+ padding: 14px 24px;
446
+ border-radius: 14px;
447
+ background: transparent;
448
+ border: 2px solid transparent;
449
+ color: var(--text-secondary);
450
+ cursor: pointer;
451
+ transition: all 0.25s;
452
+ white-space: nowrap;
453
+ font-weight: 700;
454
+ font-size: 14px;
455
+ display: flex;
456
+ align-items: center;
457
+ gap: 10px;
458
+ position: relative;
459
+ }
460
+
461
+ .tab:hover:not(.active) {
462
+ background: var(--bg-hover);
463
+ color: var(--text-primary);
464
+ border-color: var(--border);
465
+ }
466
+
467
+ .tab.active {
468
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
469
+ color: white;
470
+ box-shadow: var(--glow) var(--info-glow);
471
+ transform: scale(1.05);
472
+ border-color: var(--accent-blue);
473
+ }
474
+
475
+ .tab .icon {
476
+ width: 18px;
477
+ height: 18px;
478
+ stroke: currentColor;
479
+ stroke-width: 2.5;
480
+ stroke-linecap: round;
481
+ stroke-linejoin: round;
482
+ fill: none;
483
+ }
484
+
485
+ /* Tab Content */
486
+ .tab-content {
487
+ display: none;
488
+ animation: fadeIn 0.5s ease;
489
+ }
490
+
491
+ .tab-content.active {
492
+ display: block;
493
+ }
494
+
495
+ @keyframes fadeIn {
496
+ from { opacity: 0; transform: translateY(30px); }
497
+ to { opacity: 1; transform: translateY(0); }
498
+ }
499
+
500
+ /* Card */
501
+ .card {
502
+ background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
503
+ border: 2px solid var(--border);
504
+ border-radius: var(--radius-lg);
505
+ padding: 32px;
506
+ margin-bottom: 32px;
507
+ box-shadow: var(--shadow);
508
+ transition: var(--transition);
509
+ position: relative;
510
+ overflow: hidden;
511
+ }
512
+
513
+ .card::before {
514
+ content: '';
515
+ position: absolute;
516
+ top: 0;
517
+ left: 0;
518
+ width: 100%;
519
+ height: 100%;
520
+ background: radial-gradient(circle at top right, rgba(0, 212, 255, 0.05) 0%, transparent 70%);
521
+ pointer-events: none;
522
+ }
523
+
524
+ .card:hover {
525
+ box-shadow: var(--shadow-lg);
526
+ border-color: var(--accent-blue);
527
+ }
528
+
529
+ .card-header {
530
+ display: flex;
531
+ align-items: center;
532
+ justify-content: space-between;
533
+ margin-bottom: 28px;
534
+ padding-bottom: 20px;
535
+ border-bottom: 2px solid var(--border);
536
+ }
537
+
538
+ .card-title {
539
+ font-size: 22px;
540
+ font-weight: 800;
541
+ display: flex;
542
+ align-items: center;
543
+ gap: 14px;
544
+ color: var(--text-primary);
545
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
546
+ -webkit-background-clip: text;
547
+ -webkit-text-fill-color: transparent;
548
+ background-clip: text;
549
+ }
550
+
551
+ /* Table */
552
+ .table-container {
553
+ overflow-x: auto;
554
+ border-radius: var(--radius);
555
+ border: 2px solid var(--border);
556
+ background: var(--bg-secondary);
557
+ }
558
+
559
+ .table {
560
+ width: 100%;
561
+ border-collapse: collapse;
562
+ }
563
+
564
+ .table thead {
565
+ background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
566
+ position: sticky;
567
+ top: 0;
568
+ z-index: 10;
569
+ }
570
+
571
+ .table thead th {
572
+ color: white;
573
+ font-weight: 700;
574
+ font-size: 13px;
575
+ text-align: left;
576
+ padding: 18px 20px;
577
+ text-transform: uppercase;
578
+ letter-spacing: 1px;
579
+ border-bottom: 2px solid rgba(255, 255, 255, 0.2);
580
+ }
581
+
582
+ .table tbody tr {
583
+ transition: var(--transition);
584
+ border-bottom: 1px solid var(--border);
585
+ }
586
+
587
+ .table tbody tr:hover {
588
+ background: var(--bg-hover);
589
+ transform: scale(1.01);
590
+ box-shadow: 0 4px 12px rgba(0, 212, 255, 0.1);
591
+ }
592
+
593
+ .table tbody td {
594
+ padding: 18px 20px;
595
+ font-size: 14px;
596
+ color: var(--text-secondary);
597
+ }
598
+
599
+ .table tbody td strong {
600
+ color: var(--text-primary);
601
+ font-weight: 700;
602
+ }
603
+
604
+ /* Badge */
605
+ .badge {
606
+ display: inline-flex;
607
+ align-items: center;
608
+ gap: 8px;
609
+ padding: 8px 14px;
610
+ border-radius: 999px;
611
+ font-size: 12px;
612
+ font-weight: 700;
613
+ white-space: nowrap;
614
+ text-transform: uppercase;
615
+ letter-spacing: 0.5px;
616
+ }
617
+
618
+ .badge-success {
619
+ background: rgba(16, 185, 129, 0.2);
620
+ color: var(--success);
621
+ border: 2px solid var(--success);
622
+ box-shadow: var(--glow) var(--success-glow);
623
+ }
624
+
625
+ .badge-warning {
626
+ background: rgba(251, 191, 36, 0.2);
627
+ color: var(--warning);
628
+ border: 2px solid var(--warning);
629
+ box-shadow: var(--glow) var(--warning-glow);
630
+ }
631
+
632
+ .badge-danger {
633
+ background: rgba(239, 68, 68, 0.2);
634
+ color: var(--danger);
635
+ border: 2px solid var(--danger);
636
+ box-shadow: var(--glow) var(--danger-glow);
637
+ }
638
+
639
+ .badge-info {
640
+ background: rgba(0, 212, 255, 0.2);
641
+ color: var(--info);
642
+ border: 2px solid var(--info);
643
+ box-shadow: var(--glow) var(--info-glow);
644
+ }
645
+
646
+ /* Progress Bar */
647
+ .progress {
648
+ height: 14px;
649
+ background: var(--bg-secondary);
650
+ border-radius: 999px;
651
+ overflow: hidden;
652
+ margin: 10px 0;
653
+ border: 2px solid var(--border);
654
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
655
+ }
656
+
657
+ .progress-bar {
658
+ height: 100%;
659
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
660
+ border-radius: 999px;
661
+ transition: width 0.5s ease;
662
+ box-shadow: var(--glow) var(--info-glow);
663
+ position: relative;
664
+ overflow: hidden;
665
+ }
666
+
667
+ .progress-bar::after {
668
+ content: '';
669
+ position: absolute;
670
+ top: 0;
671
+ left: 0;
672
+ right: 0;
673
+ bottom: 0;
674
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
675
+ animation: progressShine 2s infinite;
676
+ }
677
+
678
+ @keyframes progressShine {
679
+ 0% { transform: translateX(-100%); }
680
+ 100% { transform: translateX(100%); }
681
+ }
682
+
683
+ .progress-bar.success {
684
+ background: linear-gradient(90deg, var(--success), #34d399);
685
+ box-shadow: var(--glow) var(--success-glow);
686
+ }
687
+
688
+ .progress-bar.warning {
689
+ background: linear-gradient(90deg, var(--warning), #fbbf24);
690
+ box-shadow: var(--glow) var(--warning-glow);
691
+ }
692
+
693
+ .progress-bar.danger {
694
+ background: linear-gradient(90deg, var(--danger), #f87171);
695
+ box-shadow: var(--glow) var(--danger-glow);
696
+ }
697
+
698
+ /* Loading */
699
+ .loading-overlay {
700
+ position: fixed;
701
+ top: 0;
702
+ left: 0;
703
+ right: 0;
704
+ bottom: 0;
705
+ background: rgba(15, 15, 35, 0.95);
706
+ backdrop-filter: blur(10px);
707
+ display: none;
708
+ align-items: center;
709
+ justify-content: center;
710
+ z-index: 9999;
711
+ }
712
+
713
+ .loading-overlay.active {
714
+ display: flex;
715
+ }
716
+
717
+ .spinner {
718
+ width: 80px;
719
+ height: 80px;
720
+ border: 6px solid var(--border);
721
+ border-top-color: var(--accent-blue);
722
+ border-right-color: var(--accent-purple);
723
+ border-radius: 50%;
724
+ animation: spin 1s linear infinite;
725
+ box-shadow: var(--glow) var(--info-glow);
726
+ }
727
+
728
+ @keyframes spin {
729
+ to { transform: rotate(360deg); }
730
+ }
731
+
732
+ /* Toast */
733
+ .toast-container {
734
+ position: fixed;
735
+ bottom: 32px;
736
+ right: 32px;
737
+ z-index: 10000;
738
+ display: flex;
739
+ flex-direction: column;
740
+ gap: 16px;
741
+ max-width: 400px;
742
+ }
743
+
744
+ .toast {
745
+ padding: 20px 24px;
746
+ border-radius: var(--radius);
747
+ background: var(--bg-card);
748
+ border: 2px solid var(--border);
749
+ box-shadow: var(--shadow-lg);
750
+ display: flex;
751
+ align-items: center;
752
+ gap: 14px;
753
+ animation: slideInRight 0.4s ease;
754
+ min-width: 320px;
755
+ }
756
+
757
+ @keyframes slideInRight {
758
+ from { transform: translateX(500px); opacity: 0; }
759
+ to { transform: translateX(0); opacity: 1; }
760
+ }
761
+
762
+ .toast.success {
763
+ border-color: var(--success);
764
+ background: rgba(16, 185, 129, 0.1);
765
+ box-shadow: var(--glow) var(--success-glow);
766
+ }
767
+ .toast.error {
768
+ border-color: var(--danger);
769
+ background: rgba(239, 68, 68, 0.1);
770
+ box-shadow: var(--glow) var(--danger-glow);
771
+ }
772
+ .toast.warning {
773
+ border-color: var(--warning);
774
+ background: rgba(251, 191, 36, 0.1);
775
+ box-shadow: var(--glow) var(--warning-glow);
776
+ }
777
+ .toast.info {
778
+ border-color: var(--info);
779
+ background: rgba(0, 212, 255, 0.1);
780
+ box-shadow: var(--glow) var(--info-glow);
781
+ }
782
+
783
+ /* Grid */
784
+ .grid { display: grid; gap: 24px; }
785
+ .grid-2 { grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); }
786
+ .grid-3 { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); }
787
+
788
+ /* Responsive */
789
+ @media (max-width: 768px) {
790
+ .container { padding: 20px; }
791
+ .header { padding: 24px; }
792
+ .header-top { flex-direction: column; align-items: flex-start; }
793
+ .kpi-grid { grid-template-columns: 1fr; }
794
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
795
+ .tabs { overflow-x: scroll; }
796
+ }
797
+
798
+ /* Icon Styles */
799
+ .icon {
800
+ stroke: currentColor;
801
+ stroke-width: 2.5;
802
+ stroke-linecap: round;
803
+ stroke-linejoin: round;
804
+ fill: none;
805
+ }
806
+
807
+ .icon-lg {
808
+ width: 32px;
809
+ height: 32px;
810
+ }
811
+
812
+ /* Provider Status Icons */
813
+ .provider-icon {
814
+ width: 48px;
815
+ height: 48px;
816
+ border-radius: 12px;
817
+ display: flex;
818
+ align-items: center;
819
+ justify-content: center;
820
+ font-size: 24px;
821
+ box-shadow: var(--shadow);
822
+ }
823
+
824
+ .provider-icon.online {
825
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));
826
+ border: 2px solid var(--success);
827
+ }
828
+
829
+ .provider-icon.offline {
830
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
831
+ border: 2px solid var(--danger);
832
+ }
833
+
834
+ .provider-icon.degraded {
835
+ background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.1));
836
+ border: 2px solid var(--warning);
837
+ }
838
+ </style>
839
+ </head>
840
+ <body>
841
+ <div class="loading-overlay" id="loadingOverlay">
842
+ <div class="spinner"></div>
843
+ </div>
844
+
845
+ <div class="toast-container" id="toastContainer"></div>
846
+
847
+ <div class="container">
848
+ <!-- Header -->
849
+ <div class="header">
850
+ <div class="header-top">
851
+ <div class="logo">
852
+ <div class="logo-icon">
853
+ <svg class="icon icon-lg" style="stroke: white; width: 40px; height: 40px;">
854
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
855
+ </svg>
856
+ </div>
857
+ <div class="logo-text">
858
+ <h1>🚀 Crypto API Monitor</h1>
859
+ <p>Real-time Cryptocurrency API Resource Monitoring</p>
860
+ </div>
861
+ </div>
862
+ <div class="header-actions">
863
+ <div class="connection-status" id="wsStatus">
864
+ <span class="status-dot"></span>
865
+ <span id="wsStatusText">Connecting...</span>
866
+ </div>
867
+ <div class="status-badge" id="systemStatus">
868
+ <span class="status-dot"></span>
869
+ <span id="systemStatusText">System Active</span>
870
+ </div>
871
+ <button class="btn" onclick="refreshAll()">
872
+ <svg class="icon">
873
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
874
+ </svg>
875
+ Refresh All
876
+ </button>
877
+ </div>
878
+ </div>
879
+
880
+ <!-- KPI Cards -->
881
+ <div class="kpi-grid" id="kpiGrid">
882
+ <div class="kpi-card">
883
+ <div class="kpi-header">
884
+ <span class="kpi-label">📊 Total APIs</span>
885
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.1));">
886
+ <svg class="icon icon-lg" style="stroke: var(--accent-blue); width: 36px; height: 36px;">
887
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
888
+ <line x1="3" y1="9" x2="21" y2="9"></line>
889
+ <line x1="9" y1="21" x2="9" y2="9"></line>
890
+ </svg>
891
+ </div>
892
+ </div>
893
+ <div class="kpi-value" id="kpiTotalAPIs">--</div>
894
+ <div class="kpi-trend trend-neutral">
895
+ <svg class="icon" style="width: 18px; height: 18px;">
896
+ <path d="M12 20V10M18 20V4M6 20v-4"></path>
897
+ </svg>
898
+ <span id="kpiTotalTrend">Loading...</span>
899
+ </div>
900
+ </div>
901
+
902
+ <div class="kpi-card">
903
+ <div class="kpi-header">
904
+ <span class="kpi-label">✅ Online</span>
905
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));">
906
+ <svg class="icon icon-lg" style="stroke: var(--success); width: 36px; height: 36px;">
907
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
908
+ <polyline points="9 12 11 14 15 10"></polyline>
909
+ </svg>
910
+ </div>
911
+ </div>
912
+ <div class="kpi-value" id="kpiOnline">--</div>
913
+ <div class="kpi-trend trend-up">
914
+ <svg class="icon" style="width: 18px; height: 18px;">
915
+ <line x1="12" y1="19" x2="12" y2="5"></line>
916
+ <polyline points="5 12 12 5 19 12"></polyline>
917
+ </svg>
918
+ <span id="kpiOnlineTrend">Loading...</span>
919
+ </div>
920
+ </div>
921
+
922
+ <div class="kpi-card">
923
+ <div class="kpi-header">
924
+ <span class="kpi-label">⚡ Avg Response</span>
925
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.1));">
926
+ <svg class="icon icon-lg" style="stroke: var(--warning); width: 36px; height: 36px;">
927
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
928
+ </svg>
929
+ </div>
930
+ </div>
931
+ <div class="kpi-value" id="kpiAvgResponse" style="font-size: 36px;">--</div>
932
+ <div class="kpi-trend trend-down">
933
+ <svg class="icon" style="width: 18px; height: 18px;">
934
+ <line x1="12" y1="5" x2="12" y2="19"></line>
935
+ <polyline points="19 12 12 19 5 12"></polyline>
936
+ </svg>
937
+ <span id="kpiResponseTrend">Loading...</span>
938
+ </div>
939
+ </div>
940
+
941
+ <div class="kpi-card">
942
+ <div class="kpi-header">
943
+ <span class="kpi-label">🕐 Last Update</span>
944
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.1));">
945
+ <svg class="icon icon-lg" style="stroke: var(--accent-purple); width: 36px; height: 36px;">
946
+ <circle cx="12" cy="12" r="10"></circle>
947
+ <polyline points="12 6 12 12 16 14"></polyline>
948
+ </svg>
949
+ </div>
950
+ </div>
951
+ <div class="kpi-value" id="kpiLastUpdate" style="font-size: 22px; line-height: 1.3;">--</div>
952
+ <div class="kpi-trend trend-neutral">
953
+ <svg class="icon" style="width: 18px; height: 18px;">
954
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
955
+ </svg>
956
+ <span>Auto-refresh</span>
957
+ </div>
958
+ </div>
959
+ </div>
960
+ </div>
961
+
962
+ <!-- Tabs -->
963
+ <div class="tabs">
964
+ <div class="tab active" onclick="switchTab(event, 'dashboard')">
965
+ <svg class="icon">
966
+ <rect x="3" y="3" width="7" height="7"></rect>
967
+ <rect x="14" y="3" width="7" height="7"></rect>
968
+ <rect x="14" y="14" width="7" height="7"></rect>
969
+ <rect x="3" y="14" width="7" height="7"></rect>
970
+ </svg>
971
+ <span>Dashboard</span>
972
+ </div>
973
+ <div class="tab" onclick="switchTab(event, 'providers')">
974
+ <svg class="icon">
975
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
976
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
977
+ <line x1="12" y1="22.08" x2="12" y2="12"></line>
978
+ </svg>
979
+ <span>Providers</span>
980
+ </div>
981
+ <div class="tab" onclick="switchTab(event, 'categories')">
982
+ <svg class="icon">
983
+ <rect x="3" y="3" width="7" height="7"></rect>
984
+ <rect x="14" y="3" width="7" height="7"></rect>
985
+ <rect x="14" y="14" width="7" height="7"></rect>
986
+ <rect x="3" y="14" width="7" height="7"></rect>
987
+ </svg>
988
+ <span>Categories</span>
989
+ </div>
990
+ <div class="tab" onclick="switchTab(event, 'logs')">
991
+ <svg class="icon">
992
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
993
+ <polyline points="14 2 14 8 20 8"></polyline>
994
+ <line x1="16" y1="13" x2="8" y2="13"></line>
995
+ <line x1="16" y1="17" x2="8" y2="17"></line>
996
+ <polyline points="10 9 9 9 8 9"></polyline>
997
+ </svg>
998
+ <span>Logs</span>
999
+ </div>
1000
+ <div class="tab" onclick="switchTab(event, 'huggingface')">
1001
+ <svg class="icon">
1002
+ <circle cx="12" cy="12" r="10"></circle>
1003
+ <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
1004
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
1005
+ </svg>
1006
+ <span>🤗 HuggingFace</span>
1007
+ </div>
1008
+ </div>
1009
+
1010
+ <!-- Dashboard Tab -->
1011
+ <div class="tab-content active" id="tab-dashboard">
1012
+ <div class="card">
1013
+ <div class="card-header">
1014
+ <h2 class="card-title">
1015
+ <svg class="icon icon-lg">
1016
+ <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
1017
+ </svg>
1018
+ System Overview
1019
+ </h2>
1020
+ <button class="btn" onclick="loadProviders()" style="padding: 10px 20px; font-size: 13px;">
1021
+ <svg class="icon">
1022
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1023
+ </svg>
1024
+ Refresh
1025
+ </button>
1026
+ </div>
1027
+ <div class="table-container">
1028
+ <table class="table">
1029
+ <thead>
1030
+ <tr>
1031
+ <th>🔌 Provider</th>
1032
+ <th>📁 Category</th>
1033
+ <th>📊 Status</th>
1034
+ <th>⚡ Response Time</th>
1035
+ <th>🕐 Last Check</th>
1036
+ </tr>
1037
+ </thead>
1038
+ <tbody id="providersTableBody">
1039
+ <tr>
1040
+ <td colspan="5" style="text-align: center; padding: 60px;">
1041
+ <div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
1042
+ <div style="color: var(--text-muted);">Loading providers...</div>
1043
+ </td>
1044
+ </tr>
1045
+ </tbody>
1046
+ </table>
1047
+ </div>
1048
+ </div>
1049
+
1050
+ <div class="grid grid-2">
1051
+ <div class="card">
1052
+ <div class="card-header">
1053
+ <h2 class="card-title">
1054
+ <svg class="icon icon-lg">
1055
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
1056
+ <polyline points="17 6 23 6 23 12"></polyline>
1057
+ </svg>
1058
+ Health Status
1059
+ </h2>
1060
+ </div>
1061
+ <div style="position: relative; height: 320px; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
1062
+ <canvas id="healthChart"></canvas>
1063
+ </div>
1064
+ </div>
1065
+
1066
+ <div class="card">
1067
+ <div class="card-header">
1068
+ <h2 class="card-title">
1069
+ <svg class="icon icon-lg">
1070
+ <circle cx="12" cy="12" r="10"></circle>
1071
+ <line x1="2" y1="12" x2="22" y2="12"></line>
1072
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
1073
+ </svg>
1074
+ Status Distribution
1075
+ </h2>
1076
+ </div>
1077
+ <div style="position: relative; height: 320px; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
1078
+ <canvas id="statusChart"></canvas>
1079
+ </div>
1080
+ </div>
1081
+ </div>
1082
+ </div>
1083
+
1084
+ <!-- Providers Tab -->
1085
+ <div class="tab-content" id="tab-providers">
1086
+ <div class="card">
1087
+ <div class="card-header">
1088
+ <h2 class="card-title">
1089
+ <svg class="icon icon-lg">
1090
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
1091
+ </svg>
1092
+ All Providers
1093
+ </h2>
1094
+ <button class="btn" onclick="loadProviders()" style="padding: 10px 20px; font-size: 13px;">
1095
+ <svg class="icon">
1096
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1097
+ </svg>
1098
+ Refresh
1099
+ </button>
1100
+ </div>
1101
+ <div id="providersDetail">
1102
+ <div style="text-align: center; padding: 60px;">
1103
+ <div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
1104
+ <div style="color: var(--text-muted);">Loading providers details...</div>
1105
+ </div>
1106
+ </div>
1107
+ </div>
1108
+ </div>
1109
+
1110
+ <!-- Categories Tab -->
1111
+ <div class="tab-content" id="tab-categories">
1112
+ <div class="card">
1113
+ <div class="card-header">
1114
+ <h2 class="card-title">
1115
+ <svg class="icon icon-lg">
1116
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
1117
+ <line x1="3" y1="9" x2="21" y2="9"></line>
1118
+ <line x1="9" y1="21" x2="9" y2="9"></line>
1119
+ </svg>
1120
+ Categories Overview
1121
+ </h2>
1122
+ <button class="btn" onclick="loadCategories()" style="padding: 10px 20px; font-size: 13px;">
1123
+ <svg class="icon">
1124
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1125
+ </svg>
1126
+ Refresh
1127
+ </button>
1128
+ </div>
1129
+ <div class="table-container">
1130
+ <table class="table">
1131
+ <thead>
1132
+ <tr>
1133
+ <th>📁 Category</th>
1134
+ <th>📊 Total Sources</th>
1135
+ <th>✅ Online</th>
1136
+ <th>💚 Health %</th>
1137
+ <th>⚡ Avg Response</th>
1138
+ <th>🕐 Last Updated</th>
1139
+ <th>📈 Status</th>
1140
+ </tr>
1141
+ </thead>
1142
+ <tbody id="categoriesTableBody">
1143
+ <tr>
1144
+ <td colspan="7" style="text-align: center; padding: 60px;">
1145
+ <div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
1146
+ <div style="color: var(--text-muted);">Loading categories...</div>
1147
+ </td>
1148
+ </tr>
1149
+ </tbody>
1150
+ </table>
1151
+ </div>
1152
+ </div>
1153
+ </div>
1154
+
1155
+ <!-- Logs Tab -->
1156
+ <div class="tab-content" id="tab-logs">
1157
+ <div class="card">
1158
+ <div class="card-header">
1159
+ <h2 class="card-title">
1160
+ <svg class="icon icon-lg">
1161
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
1162
+ <polyline points="14 2 14 8 20 8"></polyline>
1163
+ </svg>
1164
+ Connection Logs
1165
+ </h2>
1166
+ <button class="btn" onclick="loadLogs()" style="padding: 10px 20px; font-size: 13px;">
1167
+ <svg class="icon">
1168
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1169
+ </svg>
1170
+ Refresh
1171
+ </button>
1172
+ </div>
1173
+ <div class="table-container">
1174
+ <table class="table">
1175
+ <thead>
1176
+ <tr>
1177
+ <th>🕐 Timestamp</th>
1178
+ <th>🔌 Provider</th>
1179
+ <th>📝 Type</th>
1180
+ <th>📊 Status</th>
1181
+ <th>⚡ Response Time</th>
1182
+ <th>💬 Message</th>
1183
+ </tr>
1184
+ </thead>
1185
+ <tbody id="logsTableBody">
1186
+ <tr>
1187
+ <td colspan="6" style="text-align: center; padding: 60px;">
1188
+ <div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
1189
+ <div style="color: var(--text-muted);">Loading logs...</div>
1190
+ </td>
1191
+ </tr>
1192
+ </tbody>
1193
+ </table>
1194
+ </div>
1195
+ </div>
1196
+ </div>
1197
+
1198
+ <!-- HuggingFace Tab -->
1199
+ <div class="tab-content" id="tab-huggingface">
1200
+ <div class="card">
1201
+ <div class="card-header">
1202
+ <h2 class="card-title">
1203
+ <svg class="icon icon-lg">
1204
+ <circle cx="12" cy="12" r="10"></circle>
1205
+ </svg>
1206
+ 🤗 HuggingFace Health Status
1207
+ </h2>
1208
+ <button class="btn" onclick="refreshHFRegistry()" style="padding: 10px 20px; font-size: 13px;">
1209
+ <svg class="icon">
1210
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
1211
+ </svg>
1212
+ Refresh Registry
1213
+ </button>
1214
+ </div>
1215
+ <div id="hfHealthDisplay" style="padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); font-family: 'Courier New', monospace; font-size: 14px; white-space: pre-wrap; max-height: 320px; overflow-y: auto; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
1216
+ Loading HF health status...
1217
+ </div>
1218
+ </div>
1219
+
1220
+ <div class="grid grid-2">
1221
+ <div class="card">
1222
+ <div class="card-header">
1223
+ <h2 class="card-title">
1224
+ 🤖 Models Registry
1225
+ <span class="badge badge-success" id="hfModelsCount">0</span>
1226
+ </h2>
1227
+ </div>
1228
+ <div id="hfModelsList" style="max-height: 450px; overflow-y: auto;">
1229
+ <div style="text-align: center; padding: 40px;">
1230
+ <div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 16px;"></div>
1231
+ <div style="color: var(--text-muted);">Loading models...</div>
1232
+ </div>
1233
+ </div>
1234
+ </div>
1235
+
1236
+ <div class="card">
1237
+ <div class="card-header">
1238
+ <h2 class="card-title">
1239
+ 📊 Datasets Registry
1240
+ <span class="badge badge-success" id="hfDatasetsCount">0</span>
1241
+ </h2>
1242
+ </div>
1243
+ <div id="hfDatasetsList" style="max-height: 450px; overflow-y: auto;">
1244
+ <div style="text-align: center; padding: 40px;">
1245
+ <div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 16px;"></div>
1246
+ <div style="color: var(--text-muted);">Loading datasets...</div>
1247
+ </div>
1248
+ </div>
1249
+ </div>
1250
+ </div>
1251
+
1252
+ <div class="card">
1253
+ <div class="card-header">
1254
+ <h2 class="card-title">
1255
+ <svg class="icon icon-lg">
1256
+ <circle cx="11" cy="11" r="8"></circle>
1257
+ <path d="m21 21-4.35-4.35"></path>
1258
+ </svg>
1259
+ Search Registry
1260
+ </h2>
1261
+ </div>
1262
+ <div style="display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap;">
1263
+ <input type="text" id="hfSearchQuery" placeholder="Search crypto, bitcoin, sentiment..."
1264
+ style="flex: 1; min-width: 250px; padding: 14px 20px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px;"
1265
+ value="crypto">
1266
+ <select id="hfSearchKind" style="padding: 14px 20px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px;">
1267
+ <option value="models">Models</option>
1268
+ <option value="datasets">Datasets</option>
1269
+ </select>
1270
+ <button class="btn" onclick="searchHF()" style="padding: 14px 28px;">
1271
+ <svg class="icon">
1272
+ <circle cx="11" cy="11" r="8"></circle>
1273
+ <path d="m21 21-4.35-4.35"></path>
1274
+ </svg>
1275
+ Search
1276
+ </button>
1277
+ </div>
1278
+ <div id="hfSearchResults" style="max-height: 450px; overflow-y: auto; padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
1279
+ <div style="text-align: center; color: var(--text-muted);">Enter a query and click search</div>
1280
+ </div>
1281
+ </div>
1282
+
1283
+ <div class="card">
1284
+ <div class="card-header">
1285
+ <h2 class="card-title">💭 Sentiment Analysis</h2>
1286
+ </div>
1287
+ <div style="margin-bottom: 20px;">
1288
+ <label style="display: block; font-weight: 700; margin-bottom: 12px; color: var(--text-primary);">Text Samples (one per line)</label>
1289
+ <textarea id="hfSentimentTexts" rows="6"
1290
+ style="width: 100%; padding: 16px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px; font-family: inherit; resize: vertical;"
1291
+ placeholder="BTC strong breakout&#10;ETH looks weak&#10;Crypto market is bullish today">BTC strong breakout
1292
+ ETH looks weak
1293
+ Crypto market is bullish today
1294
+ Bears are taking control
1295
+ Neutral market conditions</textarea>
1296
+ </div>
1297
+ <button class="btn" onclick="runHFSentiment()">
1298
+ <svg class="icon">
1299
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
1300
+ </svg>
1301
+ Run Sentiment Analysis
1302
+ </button>
1303
+ <div id="hfSentimentVote" style="margin: 24px 0; padding: 32px; background: var(--bg-secondary); border-radius: var(--radius); text-align: center; font-size: 40px; font-weight: 900; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
1304
+ <span style="color: var(--text-muted);">—</span>
1305
+ </div>
1306
+ <div id="hfSentimentResults" style="padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); font-family: 'Courier New', monospace; font-size: 13px; white-space: pre-wrap; max-height: 450px; overflow-y: auto; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
1307
+ Results will appear here...
1308
+ </div>
1309
+ </div>
1310
+ </div>
1311
+ </div>
1312
+
1313
+ <script>
1314
+ // Configuration
1315
+ const config = {
1316
+ apiBaseUrl: '',
1317
+ wsUrl: (() => {
1318
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1319
+ const host = window.location.host;
1320
+ return `${protocol}//${host}/ws`;
1321
+ })(),
1322
+ autoRefreshInterval: 30000,
1323
+ maxRetries: 3
1324
+ };
1325
+
1326
+ // Global state
1327
+ let state = {
1328
+ ws: null,
1329
+ wsConnected: false,
1330
+ autoRefreshEnabled: true,
1331
+ charts: {},
1332
+ currentTab: 'dashboard',
1333
+ providers: [],
1334
+ categories: [],
1335
+ logs: [],
1336
+ lastUpdate: null
1337
+ };
1338
+
1339
+ // Initialize on page load
1340
+ document.addEventListener('DOMContentLoaded', function() {
1341
+ console.log('🚀 Initializing Crypto API Monitor...');
1342
+ initializeWebSocket();
1343
+ loadInitialData();
1344
+ startAutoRefresh();
1345
+ });
1346
+
1347
+ // WebSocket Connection
1348
+ function initializeWebSocket() {
1349
+ updateWSStatus('connecting');
1350
+
1351
+ try {
1352
+ state.ws = new WebSocket(config.wsUrl);
1353
+ setupWebSocketHandlers();
1354
+ } catch (error) {
1355
+ console.error('WebSocket connection failed:', error);
1356
+ updateWSStatus('disconnected');
1357
+ }
1358
+ }
1359
+
1360
+ function setupWebSocketHandlers() {
1361
+ state.ws.onopen = () => {
1362
+ console.log('✅ WebSocket connected');
1363
+ state.wsConnected = true;
1364
+ updateWSStatus('connected');
1365
+ showToast('Connected', 'Real-time data stream active', 'success');
1366
+ };
1367
+
1368
+ state.ws.onmessage = (event) => {
1369
+ try {
1370
+ const data = JSON.parse(event.data);
1371
+ handleWSMessage(data);
1372
+ } catch (error) {
1373
+ console.error('Error parsing WebSocket message:', error);
1374
+ }
1375
+ };
1376
+
1377
+ state.ws.onerror = (error) => {
1378
+ console.error('❌ WebSocket error:', error);
1379
+ updateWSStatus('disconnected');
1380
+ };
1381
+
1382
+ state.ws.onclose = () => {
1383
+ console.log('⚠️ WebSocket disconnected');
1384
+ state.wsConnected = false;
1385
+ updateWSStatus('disconnected');
1386
+ };
1387
+ }
1388
+
1389
+ function updateWSStatus(status) {
1390
+ const statusEl = document.getElementById('wsStatus');
1391
+ const textEl = document.getElementById('wsStatusText');
1392
+
1393
+ statusEl.classList.remove('connected', 'disconnected', 'connecting');
1394
+ statusEl.classList.add(status);
1395
+
1396
+ const statusText = {
1397
+ 'connected': '✓ Connected',
1398
+ 'disconnected': '✗ Disconnected',
1399
+ 'connecting': '⟳ Connecting...'
1400
+ };
1401
+
1402
+ textEl.textContent = statusText[status] || 'Unknown';
1403
+ }
1404
+
1405
+ function handleWSMessage(data) {
1406
+ console.log('📨 WebSocket message:', data.type);
1407
+
1408
+ switch(data.type) {
1409
+ case 'status_update':
1410
+ updateKPIs(data.data);
1411
+ break;
1412
+ case 'provider_status_change':
1413
+ loadProviders();
1414
+ break;
1415
+ case 'new_alert':
1416
+ showToast('Alert', data.data.message, 'warning');
1417
+ break;
1418
+ default:
1419
+ console.log('Unknown message type:', data.type);
1420
+ }
1421
+ }
1422
+
1423
+ // API Calls
1424
+ async function apiCall(endpoint, options = {}) {
1425
+ try {
1426
+ const url = `${config.apiBaseUrl}${endpoint}`;
1427
+ console.log('🌐 API Call:', url);
1428
+
1429
+ const response = await fetch(url, {
1430
+ ...options,
1431
+ headers: {
1432
+ 'Content-Type': 'application/json',
1433
+ ...options.headers
1434
+ }
1435
+ });
1436
+
1437
+ if (!response.ok) {
1438
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1439
+ }
1440
+
1441
+ const data = await response.json();
1442
+ console.log('✅ API Response:', endpoint, data);
1443
+ return data;
1444
+ } catch (error) {
1445
+ console.error(`❌ API call failed: ${endpoint}`, error);
1446
+ showToast('API Error', `Failed: ${endpoint}`, 'error');
1447
+ throw error;
1448
+ }
1449
+ }
1450
+
1451
+ async function loadInitialData() {
1452
+ showLoading();
1453
+
1454
+ try {
1455
+ console.log('📊 Loading initial data...');
1456
+
1457
+ await loadProviders();
1458
+
1459
+ initializeCharts();
1460
+
1461
+ state.lastUpdate = new Date();
1462
+ updateLastUpdateDisplay();
1463
+
1464
+ console.log('✅ Initial data loaded successfully');
1465
+ showToast('Success', 'Dashboard loaded successfully', 'success');
1466
+ } catch (error) {
1467
+ console.error('❌ Error loading initial data:', error);
1468
+ showToast('Error', 'Failed to load initial data', 'error');
1469
+ } finally {
1470
+ hideLoading();
1471
+ }
1472
+ }
1473
+
1474
+ async function loadProviders() {
1475
+ try {
1476
+ const data = await apiCall('/api/providers');
1477
+ state.providers = data;
1478
+ renderProvidersTable(data);
1479
+ renderProvidersDetail(data);
1480
+ updateStatusChart(data);
1481
+ updateKPIs(data);
1482
+ } catch (error) {
1483
+ console.error('Error loading providers:', error);
1484
+ document.getElementById('providersTableBody').innerHTML = `
1485
+ <tr>
1486
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 60px;">
1487
+ Failed to load providers. Please check if the API endpoint is available.
1488
+ </td>
1489
+ </tr>
1490
+ `;
1491
+ }
1492
+ }
1493
+
1494
+ async function loadCategories() {
1495
+ try {
1496
+ showLoading();
1497
+ const data = await apiCall('/api/categories');
1498
+ state.categories = data;
1499
+ renderCategoriesTable(data);
1500
+ showToast('Success', 'Categories loaded successfully', 'success');
1501
+ } catch (error) {
1502
+ console.error('Error loading categories:', error);
1503
+ showToast('Error', 'Failed to load categories', 'error');
1504
+ } finally {
1505
+ hideLoading();
1506
+ }
1507
+ }
1508
+
1509
+ async function loadLogs() {
1510
+ try {
1511
+ showLoading();
1512
+ const data = await apiCall('/api/logs');
1513
+ state.logs = data;
1514
+ renderLogsTable(data);
1515
+ showToast('Success', 'Logs loaded successfully', 'success');
1516
+ } catch (error) {
1517
+ console.error('Error loading logs:', error);
1518
+ showToast('Error', 'Failed to load logs', 'error');
1519
+ } finally {
1520
+ hideLoading();
1521
+ }
1522
+ }
1523
+
1524
+ // HuggingFace APIs
1525
+ async function loadHFHealth() {
1526
+ try {
1527
+ const data = await apiCall('/api/hf/health');
1528
+ document.getElementById('hfHealthDisplay').textContent = JSON.stringify(data, null, 2);
1529
+ } catch (error) {
1530
+ console.error('Error loading HF health:', error);
1531
+ document.getElementById('hfHealthDisplay').textContent = 'Error loading health status';
1532
+ }
1533
+ }
1534
+
1535
+ async function refreshHFRegistry() {
1536
+ try {
1537
+ showLoading();
1538
+ const data = await apiCall('/api/hf/refresh', { method: 'POST' });
1539
+ showToast('Success', 'HF Registry refreshed', 'success');
1540
+ loadHFModels();
1541
+ loadHFDatasets();
1542
+ } catch (error) {
1543
+ console.error('Error refreshing HF registry:', error);
1544
+ showToast('Error', 'Failed to refresh registry', 'error');
1545
+ } finally {
1546
+ hideLoading();
1547
+ }
1548
+ }
1549
+
1550
+ async function loadHFModels() {
1551
+ try {
1552
+ const data = await apiCall('/api/hf/registry?type=models');
1553
+ document.getElementById('hfModelsCount').textContent = data.length || 0;
1554
+ document.getElementById('hfModelsList').innerHTML = data.map(item => `
1555
+ <div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
1556
+ <div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">🤖 ${item.id || 'Unknown'}</div>
1557
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1558
+ </div>
1559
+ `).join('');
1560
+ } catch (error) {
1561
+ console.error('Error loading HF models:', error);
1562
+ document.getElementById('hfModelsList').innerHTML = '<div style="padding: 24px; text-align: center; color: var(--text-muted);">Error loading models</div>';
1563
+ }
1564
+ }
1565
+
1566
+ async function loadHFDatasets() {
1567
+ try {
1568
+ const data = await apiCall('/api/hf/registry?type=datasets');
1569
+ document.getElementById('hfDatasetsCount').textContent = data.length || 0;
1570
+ document.getElementById('hfDatasetsList').innerHTML = data.map(item => `
1571
+ <div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
1572
+ <div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">📊 ${item.id || 'Unknown'}</div>
1573
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1574
+ </div>
1575
+ `).join('');
1576
+ } catch (error) {
1577
+ console.error('Error loading HF datasets:', error);
1578
+ document.getElementById('hfDatasetsList').innerHTML = '<div style="padding: 24px; text-align: center; color: var(--text-muted);">Error loading datasets</div>';
1579
+ }
1580
+ }
1581
+
1582
+ async function searchHF() {
1583
+ try {
1584
+ showLoading();
1585
+ const query = document.getElementById('hfSearchQuery').value;
1586
+ const kind = document.getElementById('hfSearchKind').value;
1587
+ const data = await apiCall(`/api/hf/search?q=${encodeURIComponent(query)}&kind=${kind}`);
1588
+
1589
+ document.getElementById('hfSearchResults').innerHTML = data.map(item => `
1590
+ <div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
1591
+ <div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">${kind === 'models' ? '🤖' : '📊'} ${item.id || 'Unknown'}</div>
1592
+ <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">${item.description || 'No description'}</div>
1593
+ <div style="font-size: 11px; color: var(--accent-blue);">Downloads: ${item.downloads || 0} • Likes: ${item.likes || 0}</div>
1594
+ </div>
1595
+ `).join('');
1596
+
1597
+ showToast('Success', `Found ${data.length} results`, 'success');
1598
+ } catch (error) {
1599
+ console.error('Error searching HF:', error);
1600
+ showToast('Error', 'Search failed', 'error');
1601
+ } finally {
1602
+ hideLoading();
1603
+ }
1604
+ }
1605
+
1606
+ async function runHFSentiment() {
1607
+ try {
1608
+ showLoading();
1609
+ const texts = document.getElementById('hfSentimentTexts').value.split('\n').filter(t => t.trim());
1610
+
1611
+ const data = await apiCall('/api/hf/run-sentiment', {
1612
+ method: 'POST',
1613
+ body: JSON.stringify({ texts: texts })
1614
+ });
1615
+
1616
+ document.getElementById('hfSentimentResults').textContent = JSON.stringify(data, null, 2);
1617
+
1618
+ // Calculate overall sentiment vote
1619
+ const sentiments = data.results || [];
1620
+ const positive = sentiments.filter(s => s.sentiment === 'positive').length;
1621
+ const negative = sentiments.filter(s => s.sentiment === 'negative').length;
1622
+ const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;
1623
+
1624
+ let overall = 'NEUTRAL';
1625
+ let color = 'var(--info)';
1626
+
1627
+ if (positive > negative && positive > neutral) {
1628
+ overall = 'BULLISH 📈';
1629
+ color = 'var(--success)';
1630
+ } else if (negative > positive && negative > neutral) {
1631
+ overall = 'BEARISH 📉';
1632
+ color = 'var(--danger)';
1633
+ }
1634
+
1635
+ document.getElementById('hfSentimentVote').innerHTML = `
1636
+ <span style="color: ${color};">${overall}</span>
1637
+ <div style="font-size: 16px; margin-top: 12px; color: var(--text-muted);">
1638
+ Positive: ${positive} • Negative: ${negative} • Neutral: ${neutral}
1639
+ </div>
1640
+ `;
1641
+
1642
+ showToast('Success', 'Sentiment analysis completed', 'success');
1643
+ } catch (error) {
1644
+ console.error('Error running sentiment analysis:', error);
1645
+ showToast('Error', 'Sentiment analysis failed', 'error');
1646
+ } finally {
1647
+ hideLoading();
1648
+ }
1649
+ }
1650
+
1651
+ // Rendering Functions
1652
+ function renderProvidersTable(providers) {
1653
+ const tbody = document.getElementById('providersTableBody');
1654
+
1655
+ if (!providers || providers.length === 0) {
1656
+ tbody.innerHTML = `
1657
+ <tr>
1658
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 60px;">
1659
+ No providers found
1660
+ </td>
1661
+ </tr>
1662
+ `;
1663
+ return;
1664
+ }
1665
+
1666
+ tbody.innerHTML = providers.map(provider => `
1667
+ <tr>
1668
+ <td>
1669
+ <div style="display: flex; align-items: center; gap: 12px;">
1670
+ <div class="provider-icon ${provider.status || 'offline'}">
1671
+ ${provider.status === 'online' ? '✅' : provider.status === 'degraded' ? '⚠️' : '❌'}
1672
+ </div>
1673
+ <div>
1674
+ <strong style="font-size: 15px;">${provider.name || 'Unknown'}</strong>
1675
+ <div style="font-size: 11px; color: var(--text-muted);">${provider.base_url || ''}</div>
1676
+ </div>
1677
+ </div>
1678
+ </td>
1679
+ <td>
1680
+ <span class="badge badge-info">${provider.category || 'General'}</span>
1681
+ </td>
1682
+ <td>
1683
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1684
+ ${provider.status || 'unknown'}
1685
+ </span>
1686
+ </td>
1687
+ <td>
1688
+ <strong style="color: ${provider.response_time < 500 ? 'var(--success)' : provider.response_time < 1000 ? 'var(--warning)' : 'var(--danger)'};">
1689
+ ${provider.response_time ? provider.response_time + 'ms' : '--'}
1690
+ </strong>
1691
+ </td>
1692
+ <td>
1693
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(provider.last_checked)}</div>
1694
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(provider.last_checked)}</div>
1695
+ </td>
1696
+ </tr>
1697
+ `).join('');
1698
+ }
1699
+
1700
+ function renderProvidersDetail(providers) {
1701
+ const container = document.getElementById('providersDetail');
1702
+
1703
+ if (!providers || providers.length === 0) {
1704
+ container.innerHTML = `
1705
+ <div style="text-align: center; color: var(--text-muted); padding: 60px;">
1706
+ No providers data available
1707
+ </div>
1708
+ `;
1709
+ return;
1710
+ }
1711
+
1712
+ container.innerHTML = `
1713
+ <div class="table-container">
1714
+ <table class="table">
1715
+ <thead>
1716
+ <tr>
1717
+ <th>🔌 Provider</th>
1718
+ <th>📊 Status</th>
1719
+ <th>⚡ Response Time</th>
1720
+ <th>💚 Success Rate</th>
1721
+ <th>✅ Last Success</th>
1722
+ <th>❌ Errors (24h)</th>
1723
+ </tr>
1724
+ </thead>
1725
+ <tbody>
1726
+ ${providers.map(provider => `
1727
+ <tr>
1728
+ <td>
1729
+ <div style="display: flex; align-items: center; gap: 12px;">
1730
+ <div class="provider-icon ${provider.status || 'offline'}">
1731
+ ${provider.status === 'online' ? '✅' : provider.status === 'degraded' ? '⚠️' : '❌'}
1732
+ </div>
1733
+ <div>
1734
+ <strong style="font-size: 15px;">${provider.name || 'Unknown'}</strong>
1735
+ <div style="font-size: 11px; color: var(--text-muted);">${provider.base_url || ''}</div>
1736
+ </div>
1737
+ </div>
1738
+ </td>
1739
+ <td>
1740
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1741
+ ${provider.status || 'unknown'}
1742
+ </span>
1743
+ </td>
1744
+ <td>
1745
+ <strong style="color: ${provider.response_time < 500 ? 'var(--success)' : provider.response_time < 1000 ? 'var(--warning)' : 'var(--danger)'};">
1746
+ ${provider.response_time ? provider.response_time + 'ms' : '--'}
1747
+ </strong>
1748
+ </td>
1749
+ <td>
1750
+ <div class="progress">
1751
+ <div class="progress-bar ${getHealthClass(provider.success_rate || 0)}"
1752
+ style="width: ${provider.success_rate || 0}%"></div>
1753
+ </div>
1754
+ <small style="font-weight: 700;">${Math.round(provider.success_rate || 0)}%</small>
1755
+ </td>
1756
+ <td>${formatTimestamp(provider.last_success)}</td>
1757
+ <td>
1758
+ <span class="badge ${provider.error_count_24h > 10 ? 'badge-danger' : provider.error_count_24h > 0 ? 'badge-warning' : 'badge-success'}">
1759
+ ${provider.error_count_24h || 0}
1760
+ </span>
1761
+ </td>
1762
+ </tr>
1763
+ `).join('')}
1764
+ </tbody>
1765
+ </table>
1766
+ </div>
1767
+ `;
1768
+ }
1769
+
1770
+ function renderCategoriesTable(categories) {
1771
+ const tbody = document.getElementById('categoriesTableBody');
1772
+
1773
+ if (!categories || categories.length === 0) {
1774
+ tbody.innerHTML = `
1775
+ <tr>
1776
+ <td colspan="7" style="text-align: center; color: var(--text-muted); padding: 60px;">
1777
+ No categories found
1778
+ </td>
1779
+ </tr>
1780
+ `;
1781
+ return;
1782
+ }
1783
+
1784
+ tbody.innerHTML = categories.map(category => `
1785
+ <tr>
1786
+ <td>
1787
+ <strong style="font-size: 15px;">📁 ${category.name || 'Unnamed'}</strong>
1788
+ </td>
1789
+ <td><strong>${category.total_sources || 0}</strong></td>
1790
+ <td><strong style="color: var(--success);">${category.online || 0}</strong></td>
1791
+ <td>
1792
+ <div class="progress">
1793
+ <div class="progress-bar ${getHealthClass(category.health_percentage || 0)}"
1794
+ style="width: ${category.health_percentage || 0}%"></div>
1795
+ </div>
1796
+ <small style="font-weight: 700;">${Math.round(category.health_percentage || 0)}%</small>
1797
+ </td>
1798
+ <td><strong>${category.avg_response || '--'}ms</strong></td>
1799
+ <td>${formatTimestamp(category.last_updated)}</td>
1800
+ <td>
1801
+ <span class="badge ${getStatusBadgeClass(category.status)}">
1802
+ ${category.status || 'unknown'}
1803
+ </span>
1804
+ </td>
1805
+ </tr>
1806
+ `).join('');
1807
+ }
1808
+
1809
+ function renderLogsTable(logs) {
1810
+ const tbody = document.getElementById('logsTableBody');
1811
+
1812
+ if (!logs || logs.length === 0) {
1813
+ tbody.innerHTML = `
1814
+ <tr>
1815
+ <td colspan="6" style="text-align: center; color: var(--text-muted); padding: 60px;">
1816
+ No logs found
1817
+ </td>
1818
+ </tr>
1819
+ `;
1820
+ return;
1821
+ }
1822
+
1823
+ tbody.innerHTML = logs.map(log => `
1824
+ <tr>
1825
+ <td>
1826
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(log.timestamp)}</div>
1827
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(log.timestamp)}</div>
1828
+ </td>
1829
+ <td>
1830
+ <strong>${log.provider || 'System'}</strong>
1831
+ </td>
1832
+ <td>
1833
+ <span class="badge ${getLogTypeClass(log.type)}">
1834
+ ${log.type || 'unknown'}
1835
+ </span>
1836
+ </td>
1837
+ <td>
1838
+ <span class="badge ${getStatusBadgeClass(log.status)}">
1839
+ ${log.status || 'unknown'}
1840
+ </span>
1841
+ </td>
1842
+ <td><strong>${log.response_time ? log.response_time + 'ms' : '--'}</strong></td>
1843
+ <td>
1844
+ <div style="max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
1845
+ ${log.message || 'No message'}
1846
+ </div>
1847
+ </td>
1848
+ </tr>
1849
+ `).join('');
1850
+ }
1851
+
1852
+ // Helper functions
1853
+ function getHealthClass(percentage) {
1854
+ if (percentage >= 80) return 'success';
1855
+ if (percentage >= 60) return 'warning';
1856
+ return 'danger';
1857
+ }
1858
+
1859
+ function getStatusBadgeClass(status) {
1860
+ switch (status?.toLowerCase()) {
1861
+ case 'healthy': case 'online': case 'success': return 'badge-success';
1862
+ case 'degraded': case 'warning': return 'badge-warning';
1863
+ case 'offline': case 'error': case 'critical': return 'badge-danger';
1864
+ default: return 'badge-info';
1865
+ }
1866
+ }
1867
+
1868
+ function getLogTypeClass(type) {
1869
+ switch (type?.toLowerCase()) {
1870
+ case 'error': return 'badge-danger';
1871
+ case 'warning': return 'badge-warning';
1872
+ case 'info': case 'connection': return 'badge-info';
1873
+ case 'success': return 'badge-success';
1874
+ default: return 'badge-info';
1875
+ }
1876
+ }
1877
+
1878
+ function formatTimestamp(timestamp) {
1879
+ if (!timestamp) return '--';
1880
+ try {
1881
+ return new Date(timestamp).toLocaleString();
1882
+ } catch {
1883
+ return 'Invalid Date';
1884
+ }
1885
+ }
1886
+
1887
+ function formatTimeAgo(timestamp) {
1888
+ if (!timestamp) return '';
1889
+ try {
1890
+ const now = new Date();
1891
+ const time = new Date(timestamp);
1892
+ const diff = now - time;
1893
+
1894
+ const minutes = Math.floor(diff / 60000);
1895
+ const hours = Math.floor(diff / 3600000);
1896
+ const days = Math.floor(diff / 86400000);
1897
+
1898
+ if (days > 0) return `${days}d ago`;
1899
+ if (hours > 0) return `${hours}h ago`;
1900
+ if (minutes > 0) return `${minutes}m ago`;
1901
+ return 'Just now';
1902
+ } catch {
1903
+ return 'Unknown';
1904
+ }
1905
+ }
1906
+
1907
+ // KPI Updates
1908
+ function updateKPIs(data) {
1909
+ if (!data) return;
1910
+
1911
+ const totalAPIs = data.length || 0;
1912
+ document.getElementById('kpiTotalAPIs').textContent = totalAPIs;
1913
+ document.getElementById('kpiTotalTrend').textContent = `${totalAPIs} active`;
1914
+
1915
+ const onlineCount = data.filter(p => p.status === 'online' || p.status === 'healthy').length;
1916
+ document.getElementById('kpiOnline').textContent = onlineCount;
1917
+ document.getElementById('kpiOnlineTrend').textContent = `${Math.round((onlineCount / totalAPIs) * 100)}% uptime`;
1918
+
1919
+ const validResponses = data.filter(p => p.response_time).map(p => p.response_time);
1920
+ const avgResponse = validResponses.length > 0 ?
1921
+ Math.round(validResponses.reduce((a, b) => a + b, 0) / validResponses.length) : 0;
1922
+
1923
+ document.getElementById('kpiAvgResponse').textContent = avgResponse + 'ms';
1924
+
1925
+ const responseTrend = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
1926
+ document.getElementById('kpiResponseTrend').textContent = responseTrend;
1927
+ }
1928
+
1929
+ function updateLastUpdateDisplay() {
1930
+ if (state.lastUpdate) {
1931
+ document.getElementById('kpiLastUpdate').textContent =
1932
+ state.lastUpdate.toLocaleTimeString() + '\n' + state.lastUpdate.toLocaleDateString();
1933
+ }
1934
+ }
1935
+
1936
+ // Chart Functions
1937
+ function initializeCharts() {
1938
+ const healthCtx = document.getElementById('healthChart').getContext('2d');
1939
+ state.charts.health = new Chart(healthCtx, {
1940
+ type: 'line',
1941
+ data: {
1942
+ labels: [],
1943
+ datasets: [{
1944
+ label: 'System Health %',
1945
+ data: [],
1946
+ borderColor: '#00d4ff',
1947
+ backgroundColor: 'rgba(0, 212, 255, 0.1)',
1948
+ borderWidth: 3,
1949
+ fill: true,
1950
+ tension: 0.4
1951
+ }]
1952
+ },
1953
+ options: {
1954
+ responsive: true,
1955
+ maintainAspectRatio: false,
1956
+ plugins: {
1957
+ legend: {
1958
+ display: false
1959
+ }
1960
+ },
1961
+ scales: {
1962
+ y: {
1963
+ beginAtZero: true,
1964
+ max: 100,
1965
+ grid: {
1966
+ color: 'rgba(255, 255, 255, 0.1)'
1967
+ },
1968
+ ticks: {
1969
+ color: '#b8c1ec'
1970
+ }
1971
+ },
1972
+ x: {
1973
+ grid: {
1974
+ color: 'rgba(255, 255, 255, 0.1)'
1975
+ },
1976
+ ticks: {
1977
+ color: '#b8c1ec'
1978
+ }
1979
+ }
1980
+ }
1981
+ }
1982
+ });
1983
+
1984
+ const statusCtx = document.getElementById('statusChart').getContext('2d');
1985
+ state.charts.status = new Chart(statusCtx, {
1986
+ type: 'doughnut',
1987
+ data: {
1988
+ labels: ['Online', 'Degraded', 'Offline'],
1989
+ datasets: [{
1990
+ data: [0, 0, 0],
1991
+ backgroundColor: [
1992
+ '#10b981',
1993
+ '#fbbf24',
1994
+ '#ef4444'
1995
+ ],
1996
+ borderWidth: 0
1997
+ }]
1998
+ },
1999
+ options: {
2000
+ responsive: true,
2001
+ maintainAspectRatio: false,
2002
+ cutout: '70%',
2003
+ plugins: {
2004
+ legend: {
2005
+ position: 'bottom',
2006
+ labels: {
2007
+ color: '#b8c1ec',
2008
+ font: {
2009
+ size: 13,
2010
+ weight: 700
2011
+ }
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ });
2017
+ }
2018
+
2019
+ function updateStatusChart(providers) {
2020
+ if (!state.charts.status || !providers) return;
2021
+
2022
+ const online = providers.filter(p => p.status === 'online' || p.status === 'healthy').length;
2023
+ const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'warning').length;
2024
+ const offline = providers.filter(p => p.status === 'offline' || p.status === 'error').length;
2025
+
2026
+ state.charts.status.data.datasets[0].data = [online, degraded, offline];
2027
+ state.charts.status.update();
2028
+ }
2029
+
2030
+ // Tab Management
2031
+ function switchTab(event, tabName) {
2032
+ document.querySelectorAll('.tab').forEach(tab => {
2033
+ tab.classList.remove('active');
2034
+ });
2035
+
2036
+ document.querySelectorAll('.tab-content').forEach(content => {
2037
+ content.classList.remove('active');
2038
+ });
2039
+
2040
+ event.currentTarget.classList.add('active');
2041
+
2042
+ document.getElementById(`tab-${tabName}`).classList.add('active');
2043
+
2044
+ switch(tabName) {
2045
+ case 'dashboard':
2046
+ loadProviders();
2047
+ break;
2048
+ case 'providers':
2049
+ loadProviders();
2050
+ break;
2051
+ case 'categories':
2052
+ loadCategories();
2053
+ break;
2054
+ case 'logs':
2055
+ loadLogs();
2056
+ break;
2057
+ case 'huggingface':
2058
+ loadHFHealth();
2059
+ loadHFModels();
2060
+ loadHFDatasets();
2061
+ break;
2062
+ }
2063
+
2064
+ state.currentTab = tabName;
2065
+ }
2066
+
2067
+ // Utility Functions
2068
+ function showLoading() {
2069
+ document.getElementById('loadingOverlay').classList.add('active');
2070
+ }
2071
+
2072
+ function hideLoading() {
2073
+ document.getElementById('loadingOverlay').classList.remove('active');
2074
+ }
2075
+
2076
+ function showToast(title, message, type = 'info') {
2077
+ const container = document.getElementById('toastContainer');
2078
+ const toast = document.createElement('div');
2079
+ toast.className = `toast ${type}`;
2080
+ toast.innerHTML = `
2081
+ <div style="flex: 1;">
2082
+ <div style="font-weight: 700; font-size: 15px; margin-bottom: 4px;">${title}</div>
2083
+ <div style="font-size: 13px; color: var(--text-secondary);">${message}</div>
2084
+ </div>
2085
+ <button onclick="this.parentElement.remove()" style="background: none; border: none; cursor: pointer; color: inherit; padding: 4px;">
2086
+ <svg class="icon" style="width: 18px; height: 18px;">
2087
+ <line x1="18" y1="6" x2="6" y2="18"></line>
2088
+ <line x1="6" y1="6" x2="18" y2="18"></line>
2089
+ </svg>
2090
+ </button>
2091
+ `;
2092
+
2093
+ container.appendChild(toast);
2094
+
2095
+ setTimeout(() => {
2096
+ if (toast.parentElement) {
2097
+ toast.remove();
2098
+ }
2099
+ }, 5000);
2100
+ }
2101
+
2102
+ function refreshAll() {
2103
+ console.log('🔄 Refreshing all data...');
2104
+ loadInitialData();
2105
+
2106
+ switch(state.currentTab) {
2107
+ case 'categories':
2108
+ loadCategories();
2109
+ break;
2110
+ case 'logs':
2111
+ loadLogs();
2112
+ break;
2113
+ case 'huggingface':
2114
+ loadHFHealth();
2115
+ loadHFModels();
2116
+ loadHFDatasets();
2117
+ break;
2118
+ }
2119
+ }
2120
+
2121
+ function startAutoRefresh() {
2122
+ setInterval(() => {
2123
+ if (state.autoRefreshEnabled && state.wsConnected) {
2124
+ console.log('🔄 Auto-refreshing data...');
2125
+ refreshAll();
2126
+ }
2127
+ }, config.autoRefreshInterval);
2128
+ }
2129
+ </script>
2130
+ </body>
2131
+ </html>
2132
+
archive_html/pool_management.html ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Source Pool Management - Crypto API Monitor</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root {
10
+ --bg-primary: #f8fafc;
11
+ --bg-secondary: #ffffff;
12
+ --bg-card: #ffffff;
13
+ --bg-hover: #f1f5f9;
14
+ --text-primary: #0f172a;
15
+ --text-secondary: #475569;
16
+ --text-muted: #94a3b8;
17
+ --accent-primary: #3b82f6;
18
+ --accent-secondary: #8b5cf6;
19
+ --success: #10b981;
20
+ --success-bg: #d1fae5;
21
+ --warning: #f59e0b;
22
+ --warning-bg: #fef3c7;
23
+ --danger: #ef4444;
24
+ --danger-bg: #fee2e2;
25
+ --info: #06b6d4;
26
+ --info-bg: #cffafe;
27
+ --border: #e2e8f0;
28
+ --shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
29
+ --radius: 12px;
30
+ }
31
+
32
+ * {
33
+ margin: 0;
34
+ padding: 0;
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ body {
39
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
40
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
41
+ color: var(--text-primary);
42
+ line-height: 1.6;
43
+ min-height: 100vh;
44
+ padding: 20px;
45
+ }
46
+
47
+ .container {
48
+ max-width: 1600px;
49
+ margin: 0 auto;
50
+ }
51
+
52
+ .header {
53
+ background: var(--bg-secondary);
54
+ border-radius: var(--radius);
55
+ padding: 24px;
56
+ margin-bottom: 24px;
57
+ box-shadow: var(--shadow);
58
+ }
59
+
60
+ .header h1 {
61
+ font-size: 28px;
62
+ font-weight: 800;
63
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
64
+ -webkit-background-clip: text;
65
+ -webkit-text-fill-color: transparent;
66
+ margin-bottom: 8px;
67
+ }
68
+
69
+ .header p {
70
+ color: var(--text-muted);
71
+ font-size: 14px;
72
+ }
73
+
74
+ .header-actions {
75
+ display: flex;
76
+ gap: 12px;
77
+ margin-top: 16px;
78
+ }
79
+
80
+ .btn {
81
+ padding: 10px 20px;
82
+ border-radius: 8px;
83
+ border: none;
84
+ font-weight: 600;
85
+ cursor: pointer;
86
+ transition: all 0.3s;
87
+ font-size: 14px;
88
+ }
89
+
90
+ .btn-primary {
91
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
92
+ color: white;
93
+ }
94
+
95
+ .btn-primary:hover {
96
+ transform: translateY(-2px);
97
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
98
+ }
99
+
100
+ .btn-secondary {
101
+ background: var(--bg-hover);
102
+ color: var(--text-primary);
103
+ }
104
+
105
+ .btn-danger {
106
+ background: var(--danger);
107
+ color: white;
108
+ }
109
+
110
+ .btn-sm {
111
+ padding: 6px 12px;
112
+ font-size: 12px;
113
+ }
114
+
115
+ .pools-grid {
116
+ display: grid;
117
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
118
+ gap: 20px;
119
+ margin-bottom: 24px;
120
+ }
121
+
122
+ .pool-card {
123
+ background: var(--bg-card);
124
+ border-radius: var(--radius);
125
+ padding: 20px;
126
+ box-shadow: var(--shadow);
127
+ transition: all 0.3s;
128
+ }
129
+
130
+ .pool-card:hover {
131
+ transform: translateY(-4px);
132
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
133
+ }
134
+
135
+ .pool-header {
136
+ display: flex;
137
+ justify-content: space-between;
138
+ align-items: flex-start;
139
+ margin-bottom: 16px;
140
+ }
141
+
142
+ .pool-title {
143
+ font-size: 18px;
144
+ font-weight: 700;
145
+ color: var(--text-primary);
146
+ margin-bottom: 4px;
147
+ }
148
+
149
+ .pool-category {
150
+ display: inline-block;
151
+ padding: 4px 12px;
152
+ border-radius: 999px;
153
+ background: var(--info-bg);
154
+ color: var(--info);
155
+ font-size: 12px;
156
+ font-weight: 600;
157
+ }
158
+
159
+ .pool-stats {
160
+ display: grid;
161
+ grid-template-columns: repeat(2, 1fr);
162
+ gap: 12px;
163
+ margin: 16px 0;
164
+ }
165
+
166
+ .stat-item {
167
+ background: var(--bg-hover);
168
+ padding: 12px;
169
+ border-radius: 8px;
170
+ }
171
+
172
+ .stat-label {
173
+ font-size: 12px;
174
+ color: var(--text-muted);
175
+ margin-bottom: 4px;
176
+ }
177
+
178
+ .stat-value {
179
+ font-size: 20px;
180
+ font-weight: 700;
181
+ color: var(--text-primary);
182
+ }
183
+
184
+ .pool-members {
185
+ margin-top: 16px;
186
+ }
187
+
188
+ .member-item {
189
+ display: flex;
190
+ justify-content: space-between;
191
+ align-items: center;
192
+ padding: 10px;
193
+ background: var(--bg-hover);
194
+ border-radius: 8px;
195
+ margin-bottom: 8px;
196
+ }
197
+
198
+ .member-name {
199
+ font-weight: 600;
200
+ color: var(--text-primary);
201
+ }
202
+
203
+ .member-stats {
204
+ display: flex;
205
+ gap: 12px;
206
+ font-size: 12px;
207
+ color: var(--text-muted);
208
+ }
209
+
210
+ .status-indicator {
211
+ width: 10px;
212
+ height: 10px;
213
+ border-radius: 50%;
214
+ display: inline-block;
215
+ margin-right: 8px;
216
+ }
217
+
218
+ .status-online {
219
+ background: var(--success);
220
+ }
221
+
222
+ .status-warning {
223
+ background: var(--warning);
224
+ }
225
+
226
+ .status-offline {
227
+ background: var(--danger);
228
+ }
229
+
230
+ .rotation-history {
231
+ background: var(--bg-card);
232
+ border-radius: var(--radius);
233
+ padding: 20px;
234
+ box-shadow: var(--shadow);
235
+ }
236
+
237
+ .history-item {
238
+ display: flex;
239
+ align-items: center;
240
+ padding: 12px;
241
+ border-left: 3px solid var(--accent-primary);
242
+ background: var(--bg-hover);
243
+ border-radius: 4px;
244
+ margin-bottom: 12px;
245
+ }
246
+
247
+ .history-time {
248
+ font-size: 12px;
249
+ color: var(--text-muted);
250
+ margin-bottom: 4px;
251
+ }
252
+
253
+ .history-desc {
254
+ font-size: 14px;
255
+ color: var(--text-primary);
256
+ }
257
+
258
+ .modal {
259
+ display: none;
260
+ position: fixed;
261
+ top: 0;
262
+ left: 0;
263
+ width: 100%;
264
+ height: 100%;
265
+ background: rgba(0, 0, 0, 0.5);
266
+ z-index: 1000;
267
+ align-items: center;
268
+ justify-content: center;
269
+ }
270
+
271
+ .modal.active {
272
+ display: flex;
273
+ }
274
+
275
+ .modal-content {
276
+ background: var(--bg-secondary);
277
+ border-radius: var(--radius);
278
+ padding: 32px;
279
+ max-width: 600px;
280
+ width: 90%;
281
+ max-height: 90vh;
282
+ overflow-y: auto;
283
+ }
284
+
285
+ .modal-header {
286
+ font-size: 24px;
287
+ font-weight: 700;
288
+ margin-bottom: 24px;
289
+ }
290
+
291
+ .form-group {
292
+ margin-bottom: 20px;
293
+ }
294
+
295
+ .form-label {
296
+ display: block;
297
+ font-weight: 600;
298
+ margin-bottom: 8px;
299
+ color: var(--text-primary);
300
+ }
301
+
302
+ .form-input, .form-select, .form-textarea {
303
+ width: 100%;
304
+ padding: 12px;
305
+ border: 2px solid var(--border);
306
+ border-radius: 8px;
307
+ font-family: inherit;
308
+ font-size: 14px;
309
+ transition: all 0.3s;
310
+ }
311
+
312
+ .form-input:focus, .form-select:focus, .form-textarea:focus {
313
+ outline: none;
314
+ border-color: var(--accent-primary);
315
+ }
316
+
317
+ .form-textarea {
318
+ resize: vertical;
319
+ min-height: 100px;
320
+ }
321
+
322
+ .alert {
323
+ padding: 12px 16px;
324
+ border-radius: 8px;
325
+ margin-bottom: 16px;
326
+ font-size: 14px;
327
+ }
328
+
329
+ .alert-success {
330
+ background: var(--success-bg);
331
+ color: var(--success);
332
+ border-left: 4px solid var(--success);
333
+ }
334
+
335
+ .alert-error {
336
+ background: var(--danger-bg);
337
+ color: var(--danger);
338
+ border-left: 4px solid var(--danger);
339
+ }
340
+
341
+ .badge {
342
+ display: inline-block;
343
+ padding: 4px 10px;
344
+ border-radius: 999px;
345
+ font-size: 12px;
346
+ font-weight: 600;
347
+ }
348
+
349
+ .badge-success {
350
+ background: var(--success-bg);
351
+ color: var(--success);
352
+ }
353
+
354
+ .badge-warning {
355
+ background: var(--warning-bg);
356
+ color: var(--warning);
357
+ }
358
+
359
+ .badge-danger {
360
+ background: var(--danger-bg);
361
+ color: var(--danger);
362
+ }
363
+
364
+ .rate-limit-bar {
365
+ height: 8px;
366
+ background: var(--bg-hover);
367
+ border-radius: 4px;
368
+ overflow: hidden;
369
+ margin-top: 4px;
370
+ }
371
+
372
+ .rate-limit-fill {
373
+ height: 100%;
374
+ background: linear-gradient(90deg, var(--success) 0%, var(--warning) 50%, var(--danger) 100%);
375
+ transition: width 0.3s;
376
+ }
377
+ </style>
378
+ </head>
379
+ <body>
380
+ <div class="container">
381
+ <div class="header">
382
+ <h1>🔄 Source Pool Management</h1>
383
+ <p>Intelligent API source rotation and failover management</p>
384
+ <div class="header-actions">
385
+ <button class="btn btn-primary" onclick="showCreatePoolModal()">➕ Create New Pool</button>
386
+ <button class="btn btn-secondary" onclick="loadPools()">🔄 Refresh</button>
387
+ <button class="btn btn-secondary" onclick="window.location.href='index.html'">← Back to Dashboard</button>
388
+ </div>
389
+ </div>
390
+
391
+ <div id="alertContainer"></div>
392
+
393
+ <div id="poolsContainer" class="pools-grid">
394
+ <!-- Pools will be loaded here -->
395
+ </div>
396
+
397
+ <div class="rotation-history" style="margin-top: 24px;">
398
+ <h2 style="margin-bottom: 16px;">Recent Rotation Events</h2>
399
+ <div id="historyContainer">
400
+ <!-- History will be loaded here -->
401
+ </div>
402
+ </div>
403
+ </div>
404
+
405
+ <!-- Create Pool Modal -->
406
+ <div id="createPoolModal" class="modal">
407
+ <div class="modal-content">
408
+ <h2 class="modal-header">Create New Source Pool</h2>
409
+ <form id="createPoolForm">
410
+ <div class="form-group">
411
+ <label class="form-label">Pool Name</label>
412
+ <input type="text" class="form-input" id="poolName" required>
413
+ </div>
414
+ <div class="form-group">
415
+ <label class="form-label">Category</label>
416
+ <select class="form-select" id="poolCategory" required>
417
+ <option value="market_data">Market Data</option>
418
+ <option value="blockchain_explorers">Blockchain Explorers</option>
419
+ <option value="news">News</option>
420
+ <option value="sentiment">Sentiment</option>
421
+ <option value="onchain_analytics">On-Chain Analytics</option>
422
+ <option value="rpc_nodes">RPC Nodes</option>
423
+ </select>
424
+ </div>
425
+ <div class="form-group">
426
+ <label class="form-label">Rotation Strategy</label>
427
+ <select class="form-select" id="rotationStrategy" required>
428
+ <option value="round_robin">Round Robin</option>
429
+ <option value="least_used">Least Used</option>
430
+ <option value="priority">Priority Based</option>
431
+ <option value="weighted">Weighted</option>
432
+ </select>
433
+ </div>
434
+ <div class="form-group">
435
+ <label class="form-label">Description</label>
436
+ <textarea class="form-textarea" id="poolDescription"></textarea>
437
+ </div>
438
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
439
+ <button type="button" class="btn btn-secondary" onclick="closeCreatePoolModal()">Cancel</button>
440
+ <button type="submit" class="btn btn-primary">Create Pool</button>
441
+ </div>
442
+ </form>
443
+ </div>
444
+ </div>
445
+
446
+ <!-- Add Member Modal -->
447
+ <div id="addMemberModal" class="modal">
448
+ <div class="modal-content">
449
+ <h2 class="modal-header">Add Provider to Pool</h2>
450
+ <form id="addMemberForm">
451
+ <div class="form-group">
452
+ <label class="form-label">Provider</label>
453
+ <select class="form-select" id="memberProvider" required>
454
+ <!-- Will be populated dynamically -->
455
+ </select>
456
+ </div>
457
+ <div class="form-group">
458
+ <label class="form-label">Priority (higher = better)</label>
459
+ <input type="number" class="form-input" id="memberPriority" value="1" min="1" max="10">
460
+ </div>
461
+ <div class="form-group">
462
+ <label class="form-label">Weight</label>
463
+ <input type="number" class="form-input" id="memberWeight" value="1" min="1" max="100">
464
+ </div>
465
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
466
+ <button type="button" class="btn btn-secondary" onclick="closeAddMemberModal()">Cancel</button>
467
+ <button type="submit" class="btn btn-primary">Add Member</button>
468
+ </div>
469
+ </form>
470
+ </div>
471
+ </div>
472
+
473
+ <script>
474
+ const API_BASE = window.location.origin;
475
+ let currentPoolId = null;
476
+ let allProviders = [];
477
+
478
+ // Load pools on page load
479
+ document.addEventListener('DOMContentLoaded', () => {
480
+ loadPools();
481
+ loadProviders();
482
+ });
483
+
484
+ async function loadPools() {
485
+ try {
486
+ const response = await fetch(`${API_BASE}/api/pools`);
487
+ const data = await response.json();
488
+
489
+ const container = document.getElementById('poolsContainer');
490
+ container.innerHTML = '';
491
+
492
+ if (data.pools.length === 0) {
493
+ container.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">No pools configured. Create your first pool to get started.</p>';
494
+ return;
495
+ }
496
+
497
+ data.pools.forEach(pool => {
498
+ container.appendChild(createPoolCard(pool));
499
+ });
500
+ } catch (error) {
501
+ console.error('Error loading pools:', error);
502
+ showAlert('Failed to load pools', 'error');
503
+ }
504
+ }
505
+
506
+ function createPoolCard(pool) {
507
+ const card = document.createElement('div');
508
+ card.className = 'pool-card';
509
+
510
+ const currentProvider = pool.current_provider
511
+ ? `<div style="margin-bottom: 12px;">
512
+ <span class="status-indicator status-online"></span>
513
+ Current: <strong>${pool.current_provider.name}</strong>
514
+ </div>`
515
+ : '<div style="margin-bottom: 12px; color: var(--text-muted);">No active provider</div>';
516
+
517
+ const membersHTML = pool.members.map(member => {
518
+ const successRate = member.success_rate || 0;
519
+ const statusClass = successRate >= 90 ? 'status-online' : successRate >= 70 ? 'status-warning' : 'status-offline';
520
+
521
+ let rateLimitHTML = '';
522
+ if (member.rate_limit) {
523
+ const percentage = member.rate_limit.percentage;
524
+ rateLimitHTML = `
525
+ <div style="margin-top: 4px;">
526
+ <div style="font-size: 11px; color: var(--text-muted);">
527
+ ${member.rate_limit.usage}/${member.rate_limit.limit} (${percentage}%)
528
+ </div>
529
+ <div class="rate-limit-bar">
530
+ <div class="rate-limit-fill" style="width: ${percentage}%"></div>
531
+ </div>
532
+ </div>
533
+ `;
534
+ }
535
+
536
+ return `
537
+ <div class="member-item">
538
+ <div>
539
+ <span class="status-indicator ${statusClass}"></span>
540
+ <span class="member-name">${member.provider_name}</span>
541
+ <div class="member-stats">
542
+ <span>Used: ${member.use_count}</span>
543
+ <span>Success: ${successRate.toFixed(1)}%</span>
544
+ <span>Priority: ${member.priority}</span>
545
+ </div>
546
+ ${rateLimitHTML}
547
+ </div>
548
+ </div>
549
+ `;
550
+ }).join('');
551
+
552
+ card.innerHTML = `
553
+ <div class="pool-header">
554
+ <div>
555
+ <div class="pool-title">${pool.pool_name}</div>
556
+ <span class="pool-category">${pool.category}</span>
557
+ </div>
558
+ <div style="display: flex; gap: 8px;">
559
+ <button class="btn btn-sm btn-secondary" onclick="addMember(${pool.pool_id})">➕</button>
560
+ <button class="btn btn-sm btn-primary" onclick="rotatePool(${pool.pool_id})">🔄</button>
561
+ <button class="btn btn-sm btn-danger" onclick="deletePool(${pool.pool_id}, '${pool.pool_name}')">🗑️</button>
562
+ </div>
563
+ </div>
564
+
565
+ ${currentProvider}
566
+
567
+ <div class="pool-stats">
568
+ <div class="stat-item">
569
+ <div class="stat-label">Strategy</div>
570
+ <div class="stat-value" style="font-size: 14px;">${pool.rotation_strategy}</div>
571
+ </div>
572
+ <div class="stat-item">
573
+ <div class="stat-label">Total Rotations</div>
574
+ <div class="stat-value">${pool.total_rotations}</div>
575
+ </div>
576
+ <div class="stat-item">
577
+ <div class="stat-label">Members</div>
578
+ <div class="stat-value">${pool.members.length}</div>
579
+ </div>
580
+ <div class="stat-item">
581
+ <div class="stat-label">Status</div>
582
+ <div class="stat-value">
583
+ <span class="badge ${pool.enabled ? 'badge-success' : 'badge-danger'}">
584
+ ${pool.enabled ? 'Enabled' : 'Disabled'}
585
+ </span>
586
+ </div>
587
+ </div>
588
+ </div>
589
+
590
+ <div class="pool-members">
591
+ <div style="font-weight: 600; margin-bottom: 12px;">Pool Members</div>
592
+ ${membersHTML || '<div style="color: var(--text-muted); font-size: 14px;">No members</div>'}
593
+ </div>
594
+ `;
595
+
596
+ return card;
597
+ }
598
+
599
+ async function loadProviders() {
600
+ try {
601
+ const response = await fetch(`${API_BASE}/api/providers`);
602
+ const providers = await response.json();
603
+ allProviders = providers;
604
+
605
+ const select = document.getElementById('memberProvider');
606
+ select.innerHTML = providers.map(p =>
607
+ `<option value="${p.id}">${p.name} (${p.category})</option>`
608
+ ).join('');
609
+ } catch (error) {
610
+ console.error('Error loading providers:', error);
611
+ }
612
+ }
613
+
614
+ function showCreatePoolModal() {
615
+ document.getElementById('createPoolModal').classList.add('active');
616
+ }
617
+
618
+ function closeCreatePoolModal() {
619
+ document.getElementById('createPoolModal').classList.remove('active');
620
+ document.getElementById('createPoolForm').reset();
621
+ }
622
+
623
+ function showAddMemberModal() {
624
+ document.getElementById('addMemberModal').classList.add('active');
625
+ }
626
+
627
+ function closeAddMemberModal() {
628
+ document.getElementById('addMemberModal').classList.remove('active');
629
+ document.getElementById('addMemberForm').reset();
630
+ }
631
+
632
+ function addMember(poolId) {
633
+ currentPoolId = poolId;
634
+ showAddMemberModal();
635
+ }
636
+
637
+ document.getElementById('createPoolForm').addEventListener('submit', async (e) => {
638
+ e.preventDefault();
639
+
640
+ const data = {
641
+ name: document.getElementById('poolName').value,
642
+ category: document.getElementById('poolCategory').value,
643
+ rotation_strategy: document.getElementById('rotationStrategy').value,
644
+ description: document.getElementById('poolDescription').value
645
+ };
646
+
647
+ try {
648
+ const response = await fetch(`${API_BASE}/api/pools`, {
649
+ method: 'POST',
650
+ headers: { 'Content-Type': 'application/json' },
651
+ body: JSON.stringify(data)
652
+ });
653
+
654
+ if (response.ok) {
655
+ showAlert('Pool created successfully', 'success');
656
+ closeCreatePoolModal();
657
+ loadPools();
658
+ } else {
659
+ const error = await response.json();
660
+ showAlert(error.detail || 'Failed to create pool', 'error');
661
+ }
662
+ } catch (error) {
663
+ console.error('Error creating pool:', error);
664
+ showAlert('Failed to create pool', 'error');
665
+ }
666
+ });
667
+
668
+ document.getElementById('addMemberForm').addEventListener('submit', async (e) => {
669
+ e.preventDefault();
670
+
671
+ const data = {
672
+ provider_id: parseInt(document.getElementById('memberProvider').value),
673
+ priority: parseInt(document.getElementById('memberPriority').value),
674
+ weight: parseInt(document.getElementById('memberWeight').value)
675
+ };
676
+
677
+ try {
678
+ const response = await fetch(`${API_BASE}/api/pools/${currentPoolId}/members`, {
679
+ method: 'POST',
680
+ headers: { 'Content-Type': 'application/json' },
681
+ body: JSON.stringify(data)
682
+ });
683
+
684
+ if (response.ok) {
685
+ showAlert('Member added successfully', 'success');
686
+ closeAddMemberModal();
687
+ loadPools();
688
+ } else {
689
+ const error = await response.json();
690
+ showAlert(error.detail || 'Failed to add member', 'error');
691
+ }
692
+ } catch (error) {
693
+ console.error('Error adding member:', error);
694
+ showAlert('Failed to add member', 'error');
695
+ }
696
+ });
697
+
698
+ async function rotatePool(poolId) {
699
+ try {
700
+ const response = await fetch(`${API_BASE}/api/pools/${poolId}/rotate`, {
701
+ method: 'POST',
702
+ headers: { 'Content-Type': 'application/json' },
703
+ body: JSON.stringify({ reason: 'manual' })
704
+ });
705
+
706
+ if (response.ok) {
707
+ const result = await response.json();
708
+ showAlert(`Rotated to ${result.provider_name}`, 'success');
709
+ loadPools();
710
+ } else {
711
+ const error = await response.json();
712
+ showAlert(error.detail || 'Failed to rotate', 'error');
713
+ }
714
+ } catch (error) {
715
+ console.error('Error rotating pool:', error);
716
+ showAlert('Failed to rotate pool', 'error');
717
+ }
718
+ }
719
+
720
+ async function deletePool(poolId, poolName) {
721
+ if (!confirm(`Are you sure you want to delete pool "${poolName}"?`)) {
722
+ return;
723
+ }
724
+
725
+ try {
726
+ const response = await fetch(`${API_BASE}/api/pools/${poolId}`, {
727
+ method: 'DELETE'
728
+ });
729
+
730
+ if (response.ok) {
731
+ showAlert('Pool deleted successfully', 'success');
732
+ loadPools();
733
+ } else {
734
+ const error = await response.json();
735
+ showAlert(error.detail || 'Failed to delete pool', 'error');
736
+ }
737
+ } catch (error) {
738
+ console.error('Error deleting pool:', error);
739
+ showAlert('Failed to delete pool', 'error');
740
+ }
741
+ }
742
+
743
+ function showAlert(message, type) {
744
+ const container = document.getElementById('alertContainer');
745
+ const alert = document.createElement('div');
746
+ alert.className = `alert alert-${type}`;
747
+ alert.textContent = message;
748
+ container.appendChild(alert);
749
+
750
+ setTimeout(() => {
751
+ alert.remove();
752
+ }, 5000);
753
+ }
754
+
755
+ // Close modals when clicking outside
756
+ document.querySelectorAll('.modal').forEach(modal => {
757
+ modal.addEventListener('click', (e) => {
758
+ if (e.target === modal) {
759
+ modal.classList.remove('active');
760
+ }
761
+ });
762
+ });
763
+ </script>
764
+ </body>
765
+ </html>
archive_html/simple_overview.html ADDED
@@ -0,0 +1,303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Monitor - Complete Overview</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ background: white;
28
+ border-radius: 15px;
29
+ padding: 30px;
30
+ margin-bottom: 20px;
31
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ }
36
+
37
+ .header h1 {
38
+ color: #667eea;
39
+ font-size: 2em;
40
+ }
41
+
42
+ .refresh-btn {
43
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
44
+ color: white;
45
+ border: none;
46
+ padding: 12px 30px;
47
+ border-radius: 10px;
48
+ font-size: 1em;
49
+ font-weight: 600;
50
+ cursor: pointer;
51
+ transition: all 0.3s ease;
52
+ }
53
+
54
+ .refresh-btn:hover {
55
+ transform: translateY(-2px);
56
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
57
+ }
58
+
59
+ .stats-grid {
60
+ display: grid;
61
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
62
+ gap: 15px;
63
+ margin-bottom: 20px;
64
+ }
65
+
66
+ .stat-card {
67
+ background: white;
68
+ border-radius: 12px;
69
+ padding: 20px;
70
+ text-align: center;
71
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
72
+ }
73
+
74
+ .stat-card h3 {
75
+ color: #999;
76
+ font-size: 0.85em;
77
+ text-transform: uppercase;
78
+ margin-bottom: 10px;
79
+ }
80
+
81
+ .stat-card .value {
82
+ font-size: 2.5em;
83
+ font-weight: bold;
84
+ margin-bottom: 5px;
85
+ }
86
+
87
+ .stat-card.green .value { color: #10b981; }
88
+ .stat-card.blue .value { color: #3b82f6; }
89
+ .stat-card.orange .value { color: #f59e0b; }
90
+ .stat-card.red .value { color: #ef4444; }
91
+
92
+ .content-grid {
93
+ display: grid;
94
+ grid-template-columns: 2fr 1fr;
95
+ gap: 20px;
96
+ }
97
+
98
+ .card {
99
+ background: white;
100
+ border-radius: 15px;
101
+ padding: 25px;
102
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
103
+ }
104
+
105
+ .card h2 {
106
+ color: #333;
107
+ margin-bottom: 20px;
108
+ padding-bottom: 10px;
109
+ border-bottom: 3px solid #667eea;
110
+ }
111
+
112
+ .providers-list {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
115
+ gap: 10px;
116
+ max-height: 600px;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ .provider-item {
121
+ background: #f8f9fa;
122
+ border-radius: 8px;
123
+ padding: 12px;
124
+ border-left: 4px solid #ddd;
125
+ }
126
+
127
+ .provider-item.online {
128
+ border-left-color: #10b981;
129
+ background: linear-gradient(to right, #f0fdf4, #f8f9fa);
130
+ }
131
+ .provider-item.offline {
132
+ border-left-color: #ef4444;
133
+ background: linear-gradient(to right, #fef2f2, #f8f9fa);
134
+ }
135
+ .provider-item.degraded {
136
+ border-left-color: #f59e0b;
137
+ background: linear-gradient(to right, #fffbeb, #f8f9fa);
138
+ }
139
+
140
+ .provider-item .name {
141
+ font-weight: 600;
142
+ color: #333;
143
+ font-size: 0.9em;
144
+ margin-bottom: 5px;
145
+ }
146
+
147
+ .provider-item .info {
148
+ font-size: 0.75em;
149
+ color: #666;
150
+ }
151
+
152
+ .category-list {
153
+ display: flex;
154
+ flex-direction: column;
155
+ gap: 12px;
156
+ }
157
+
158
+ .category-item {
159
+ background: #f8f9fa;
160
+ border-radius: 8px;
161
+ padding: 15px;
162
+ }
163
+
164
+ .category-item .name {
165
+ font-weight: 600;
166
+ color: #333;
167
+ margin-bottom: 8px;
168
+ }
169
+
170
+ .category-item .stats {
171
+ display: flex;
172
+ gap: 10px;
173
+ font-size: 0.85em;
174
+ }
175
+
176
+ .loading {
177
+ text-align: center;
178
+ padding: 40px;
179
+ color: #666;
180
+ }
181
+
182
+ @media (max-width: 768px) {
183
+ .content-grid {
184
+ grid-template-columns: 1fr;
185
+ }
186
+ .stats-grid {
187
+ grid-template-columns: repeat(2, 1fr);
188
+ }
189
+ }
190
+ </style>
191
+ </head>
192
+ <body>
193
+ <div class="container">
194
+ <div class="header">
195
+ <div>
196
+ <h1>🚀 Crypto API Monitor</h1>
197
+ <p style="color: #666; margin-top: 5px;">Complete System Overview</p>
198
+ </div>
199
+ <button class="refresh-btn" onclick="loadData()">🔄 Refresh</button>
200
+ </div>
201
+
202
+ <div class="stats-grid">
203
+ <div class="stat-card blue">
204
+ <h3>Total APIs</h3>
205
+ <div class="value" id="total">-</div>
206
+ </div>
207
+ <div class="stat-card green">
208
+ <h3>Online</h3>
209
+ <div class="value" id="online">-</div>
210
+ </div>
211
+ <div class="stat-card orange">
212
+ <h3>Degraded</h3>
213
+ <div class="value" id="degraded">-</div>
214
+ </div>
215
+ <div class="stat-card red">
216
+ <h3>Offline</h3>
217
+ <div class="value" id="offline">-</div>
218
+ </div>
219
+ </div>
220
+
221
+ <div class="content-grid">
222
+ <div class="card">
223
+ <h2>📊 All Providers</h2>
224
+ <div class="providers-list" id="providers">
225
+ <div class="loading">Loading...</div>
226
+ </div>
227
+ </div>
228
+
229
+ <div class="card">
230
+ <h2>📁 Categories</h2>
231
+ <div class="category-list" id="categories">
232
+ <div class="loading">Loading...</div>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <script>
239
+ async function loadData() {
240
+ try {
241
+ const response = await fetch('/api/providers');
242
+ const providers = await response.json();
243
+
244
+ // Calculate stats
245
+ const online = providers.filter(p => p.status === 'online').length;
246
+ const offline = providers.filter(p => p.status === 'offline').length;
247
+ const degraded = providers.filter(p => p.status === 'degraded').length;
248
+
249
+ // Update stats
250
+ document.getElementById('total').textContent = providers.length;
251
+ document.getElementById('online').textContent = online;
252
+ document.getElementById('degraded').textContent = degraded;
253
+ document.getElementById('offline').textContent = offline;
254
+
255
+ // Group by category
256
+ const categories = {};
257
+ providers.forEach(p => {
258
+ if (!categories[p.category]) {
259
+ categories[p.category] = { online: 0, offline: 0, degraded: 0 };
260
+ }
261
+ categories[p.category][p.status]++;
262
+ });
263
+
264
+ // Display providers
265
+ const providersHtml = providers.map(p => `
266
+ <div class="provider-item ${p.status}">
267
+ <div class="name">${p.name}</div>
268
+ <div class="info">${p.category}</div>
269
+ <div class="info" style="color: ${p.status === 'online' ? '#10b981' : p.status === 'degraded' ? '#f59e0b' : '#ef4444'}">
270
+ ${p.status.toUpperCase()}
271
+ </div>
272
+ </div>
273
+ `).join('');
274
+ document.getElementById('providers').innerHTML = providersHtml;
275
+
276
+ // Display categories
277
+ const categoriesHtml = Object.entries(categories).map(([name, stats]) => `
278
+ <div class="category-item">
279
+ <div class="name">${name}</div>
280
+ <div class="stats">
281
+ <span style="color: #10b981;">✓ ${stats.online}</span>
282
+ <span style="color: #f59e0b;">⚠ ${stats.degraded}</span>
283
+ <span style="color: #ef4444;">✗ ${stats.offline}</span>
284
+ </div>
285
+ </div>
286
+ `).join('');
287
+ document.getElementById('categories').innerHTML = categoriesHtml;
288
+
289
+ } catch (error) {
290
+ console.error('Error:', error);
291
+ document.getElementById('providers').innerHTML = '<div class="loading">Error loading data</div>';
292
+ }
293
+ }
294
+
295
+ // Load on start
296
+ loadData();
297
+
298
+ // Auto-refresh every 30 seconds
299
+ setInterval(loadData, 30000);
300
+ </script>
301
+ </body>
302
+ </html>
303
+
archive_html/unified_dashboard.html ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Monitor HF - Unified Dashboard</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
+ <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">
160
+ <div class="section-header">
161
+ <h2 class="section-title">Global Overview</h2>
162
+ <span class="chip">Powered by /api/market/stats</span>
163
+ </div>
164
+ <div class="stats-grid" data-overview-stats></div>
165
+ <div class="grid-two">
166
+ <div class="glass-card">
167
+ <div class="section-header">
168
+ <h3>Top Coins</h3>
169
+ <span class="text-muted">Market movers</span>
170
+ </div>
171
+ <div class="table-wrapper">
172
+ <table>
173
+ <thead>
174
+ <tr>
175
+ <th>#</th>
176
+ <th>Symbol</th>
177
+ <th>Name</th>
178
+ <th>Price</th>
179
+ <th>24h %</th>
180
+ <th>Volume</th>
181
+ <th>Market Cap</th>
182
+ </tr>
183
+ </thead>
184
+ <tbody data-top-coins-body></tbody>
185
+ </table>
186
+ </div>
187
+ </div>
188
+ <div class="glass-card">
189
+ <div class="section-header">
190
+ <h3>Global Sentiment</h3>
191
+ <span class="text-muted">CryptoBERT stack</span>
192
+ </div>
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">
223
+ <div class="section-header">
224
+ <h2 class="section-title">Market Intelligence</h2>
225
+ <div class="controls-bar">
226
+ <div class="input-chip">
227
+ <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>
228
+ <input type="text" placeholder="Search symbol" data-market-search />
229
+ </div>
230
+ <div class="input-chip">
231
+ Timeframe:
232
+ <button class="ghost" data-timeframe="1d">1D</button>
233
+ <button class="ghost active" data-timeframe="7d">7D</button>
234
+ <button class="ghost" data-timeframe="30d">30D</button>
235
+ </div>
236
+ <label class="input-chip"> Live updates
237
+ <div class="toggle">
238
+ <input type="checkbox" data-live-toggle />
239
+ <span></span>
240
+ </div>
241
+ </label>
242
+ </div>
243
+ </div>
244
+ <div class="glass-card">
245
+ <div class="table-wrapper">
246
+ <table>
247
+ <thead>
248
+ <tr>
249
+ <th>#</th>
250
+ <th>Symbol</th>
251
+ <th>Name</th>
252
+ <th>Price</th>
253
+ <th>24h %</th>
254
+ <th>Volume</th>
255
+ <th>Market Cap</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody data-market-body></tbody>
259
+ </table>
260
+ </div>
261
+ </div>
262
+ <div class="drawer" data-market-drawer>
263
+ <button class="ghost" data-close-drawer>Close</button>
264
+ <h3 data-drawer-symbol>—</h3>
265
+ <div data-drawer-stats></div>
266
+ <div class="glass-card" data-chart-wrapper>
267
+ <canvas id="market-detail-chart" height="180"></canvas>
268
+ </div>
269
+ <div class="glass-card">
270
+ <h4>Related Headlines</h4>
271
+ <div data-drawer-news></div>
272
+ </div>
273
+ </div>
274
+ </section>
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>
328
+
329
+ <section id="page-ai" class="page">
330
+ <div class="section-header">
331
+ <h2 class="section-title">Sentiment & AI Advisor</h2>
332
+ </div>
333
+ <div class="glass-card">
334
+ <form data-ai-form class="ai-form">
335
+ <div class="grid-two">
336
+ <label>Symbol
337
+ <select name="symbol">
338
+ <option value="BTC">BTC</option>
339
+ <option value="ETH">ETH</option>
340
+ <option value="SOL">SOL</option>
341
+ </select>
342
+ </label>
343
+ <label>Time Horizon
344
+ <select name="horizon">
345
+ <option value="intraday">Intraday</option>
346
+ <option value="swing" selected>Swing</option>
347
+ <option value="long">Long Term</option>
348
+ </select>
349
+ </label>
350
+ <label>Risk Profile
351
+ <select name="risk">
352
+ <option value="conservative">Conservative</option>
353
+ <option value="moderate" selected>Moderate</option>
354
+ <option value="aggressive">Aggressive</option>
355
+ </select>
356
+ </label>
357
+ <label>Sentiment Model
358
+ <select name="model">
359
+ <option value="auto">Auto</option>
360
+ <option value="crypto">CryptoBERT</option>
361
+ <option value="financial">FinBERT</option>
362
+ <option value="social">Twitter Sentiment</option>
363
+ </select>
364
+ </label>
365
+ </div>
366
+ <label>Context or Headline
367
+ <textarea name="context" placeholder="Paste a headline or trade thesis for AI analysis"></textarea>
368
+ </label>
369
+ <button class="primary" type="submit">Generate Guidance</button>
370
+ </form>
371
+ <div class="grid-two">
372
+ <div data-ai-result class="ai-result"></div>
373
+ <div data-sentiment-result></div>
374
+ </div>
375
+ <div class="inline-message inline-info" data-ai-disclaimer>
376
+ Experimental AI output. Not financial advice.
377
+ </div>
378
+ </div>
379
+ </section>
380
+
381
+ <section id="page-news" class="page">
382
+ <div class="section-header">
383
+ <h2 class="section-title">News & Summaries</h2>
384
+ </div>
385
+ <div class="controls-bar">
386
+ <select data-news-range>
387
+ <option value="24h">Last 24h</option>
388
+ <option value="7d">7 Days</option>
389
+ <option value="30d">30 Days</option>
390
+ </select>
391
+ <input type="text" placeholder="Search headline" data-news-search />
392
+ <input type="text" placeholder="Filter symbol (e.g. BTC)" data-news-symbol />
393
+ </div>
394
+ <div class="glass-card">
395
+ <div class="table-wrapper">
396
+ <table>
397
+ <thead>
398
+ <tr>
399
+ <th>Time</th>
400
+ <th>Source</th>
401
+ <th>Title</th>
402
+ <th>Symbols</th>
403
+ <th>Sentiment</th>
404
+ <th>AI</th>
405
+ </tr>
406
+ </thead>
407
+ <tbody data-news-body></tbody>
408
+ </table>
409
+ </div>
410
+ </div>
411
+ <div class="modal-backdrop" data-news-modal>
412
+ <div class="modal">
413
+ <button class="ghost" data-close-news-modal>Close</button>
414
+ <div data-news-modal-content></div>
415
+ </div>
416
+ </div>
417
+ </section>
418
+
419
+ <section id="page-providers" class="page">
420
+ <div class="section-header">
421
+ <h2 class="section-title">Provider Health</h2>
422
+ <button class="ghost" data-provider-refresh>Refresh</button>
423
+ </div>
424
+ <div class="stats-grid" data-provider-summary></div>
425
+ <div class="controls-bar">
426
+ <input type="search" placeholder="Search provider" data-provider-search />
427
+ <select data-provider-category>
428
+ <option value="all">All Categories</option>
429
+ <option value="market">Market Data</option>
430
+ <option value="news">News</option>
431
+ <option value="ai">AI</option>
432
+ </select>
433
+ </div>
434
+ <div class="glass-card">
435
+ <div class="table-wrapper">
436
+ <table>
437
+ <thead>
438
+ <tr>
439
+ <th>Name</th>
440
+ <th>Category</th>
441
+ <th>Status</th>
442
+ <th>Latency</th>
443
+ <th>Details</th>
444
+ </tr>
445
+ </thead>
446
+ <tbody data-providers-table></tbody>
447
+ </table>
448
+ </div>
449
+ </div>
450
+ </section>
451
+
452
+ <section id="page-api" class="page">
453
+ <div class="section-header">
454
+ <h2 class="section-title">API Explorer</h2>
455
+ <span class="chip">Test live endpoints</span>
456
+ </div>
457
+ <div class="glass-card">
458
+ <div class="grid-two">
459
+ <label>Endpoint
460
+ <select data-api-endpoint></select>
461
+ </label>
462
+ <label>Method
463
+ <select data-api-method>
464
+ <option value="GET">GET</option>
465
+ <option value="POST">POST</option>
466
+ </select>
467
+ </label>
468
+ <label>Query Params
469
+ <input type="text" placeholder="limit=10&symbol=BTC" data-api-params />
470
+ </label>
471
+ <label>Body (JSON)
472
+ <textarea data-api-body placeholder='{ "text": "Bitcoin" }'></textarea>
473
+ </label>
474
+ </div>
475
+ <p class="text-muted">Path: <span data-api-path></span> — <span data-api-description></span></p>
476
+ <button class="primary" data-api-send>Send Request</button>
477
+ <div class="inline-message" data-api-meta>Ready</div>
478
+ <pre data-api-response class="api-response"></pre>
479
+ </div>
480
+ </section>
481
+
482
+ <section id="page-debug" class="page">
483
+ <div class="section-header">
484
+ <h2 class="section-title">Diagnostics</h2>
485
+ <button class="ghost" data-refresh-health>Refresh</button>
486
+ </div>
487
+ <div class="stats-grid">
488
+ <div class="glass-card">
489
+ <h3>API Health</h3>
490
+ <div class="stat-value" data-health-status>—</div>
491
+ </div>
492
+ <div class="glass-card">
493
+ <h3>Providers</h3>
494
+ <div data-providers class="grid-two"></div>
495
+ </div>
496
+ </div>
497
+ <div class="grid-two">
498
+ <div class="glass-card">
499
+ <h4>Request Log</h4>
500
+ <div class="table-wrapper log-table">
501
+ <table>
502
+ <thead>
503
+ <tr>
504
+ <th>Time</th>
505
+ <th>Method</th>
506
+ <th>Endpoint</th>
507
+ <th>Status</th>
508
+ <th>Latency</th>
509
+ </tr>
510
+ </thead>
511
+ <tbody data-request-log></tbody>
512
+ </table>
513
+ </div>
514
+ </div>
515
+ <div class="glass-card">
516
+ <h4>Error Log</h4>
517
+ <div class="table-wrapper log-table">
518
+ <table>
519
+ <thead>
520
+ <tr>
521
+ <th>Time</th>
522
+ <th>Endpoint</th>
523
+ <th>Message</th>
524
+ </tr>
525
+ </thead>
526
+ <tbody data-error-log></tbody>
527
+ </table>
528
+ </div>
529
+ </div>
530
+ </div>
531
+ <div class="glass-card">
532
+ <h4>WebSocket Events</h4>
533
+ <div class="table-wrapper log-table">
534
+ <table>
535
+ <thead>
536
+ <tr>
537
+ <th>Time</th>
538
+ <th>Type</th>
539
+ <th>Detail</th>
540
+ </tr>
541
+ </thead>
542
+ <tbody data-ws-log></tbody>
543
+ </table>
544
+ </div>
545
+ </div>
546
+ </section>
547
+
548
+ <section id="page-datasets" class="page">
549
+ <div class="section-header">
550
+ <h2 class="section-title">Datasets & Models</h2>
551
+ </div>
552
+ <div class="grid-two">
553
+ <div class="glass-card">
554
+ <h3>Datasets</h3>
555
+ <div class="table-wrapper">
556
+ <table>
557
+ <thead>
558
+ <tr>
559
+ <th>Name</th>
560
+ <th>Records</th>
561
+ <th>Updated</th>
562
+ <th>Actions</th>
563
+ </tr>
564
+ </thead>
565
+ <tbody data-datasets-body></tbody>
566
+ </table>
567
+ </div>
568
+ </div>
569
+ <div class="glass-card">
570
+ <h3>Models</h3>
571
+ <div class="table-wrapper">
572
+ <table>
573
+ <thead>
574
+ <tr>
575
+ <th>Name</th>
576
+ <th>Task</th>
577
+ <th>Status</th>
578
+ <th>Notes</th>
579
+ </tr>
580
+ </thead>
581
+ <tbody data-models-body></tbody>
582
+ </table>
583
+ </div>
584
+ </div>
585
+ </div>
586
+ <div class="glass-card">
587
+ <h4>Test a Model</h4>
588
+ <form data-model-test-form class="grid-two">
589
+ <label>Model
590
+ <select data-model-select name="model"></select>
591
+ </label>
592
+ <label>Input
593
+ <textarea name="input" placeholder="Type a prompt"></textarea>
594
+ </label>
595
+ <button class="primary" type="submit">Run Test</button>
596
+ </form>
597
+ <div data-model-test-output></div>
598
+ </div>
599
+ <div class="modal-backdrop" data-dataset-modal>
600
+ <div class="modal">
601
+ <button class="ghost" data-close-dataset-modal>Close</button>
602
+ <div data-dataset-modal-content></div>
603
+ </div>
604
+ </div>
605
+ </section>
606
+
607
+ <section id="page-settings" class="page">
608
+ <div class="section-header">
609
+ <h2 class="section-title">Settings</h2>
610
+ </div>
611
+ <div class="glass-card">
612
+ <div class="grid-two">
613
+ <label class="input-chip">Light Theme
614
+ <div class="toggle">
615
+ <input type="checkbox" data-theme-toggle />
616
+ <span></span>
617
+ </div>
618
+ </label>
619
+ <label>Market Refresh (sec)
620
+ <input type="number" min="15" step="5" data-market-interval />
621
+ </label>
622
+ <label>News Refresh (sec)
623
+ <input type="number" min="30" step="10" data-news-interval />
624
+ </label>
625
+ <label class="input-chip">Compact Layout
626
+ <div class="toggle">
627
+ <input type="checkbox" data-layout-toggle />
628
+ <span></span>
629
+ </div>
630
+ </label>
631
+ </div>
632
+ </div>
633
+ </section>
634
+ </div>
635
+ </main>
636
+ </div>
637
+ <script type="module" src="static/js/app.js"></script>
638
+ </body>
639
+ </html>
backend/routers/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/routers/__pycache__/__init__.cpython-313.pyc and b/backend/routers/__pycache__/__init__.cpython-313.pyc differ
 
backend/routers/__pycache__/hf_connect.cpython-313.pyc CHANGED
Binary files a/backend/routers/__pycache__/hf_connect.cpython-313.pyc and b/backend/routers/__pycache__/hf_connect.cpython-313.pyc differ
 
backend/services/__pycache__/hf_client.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/hf_client.cpython-313.pyc and b/backend/services/__pycache__/hf_client.cpython-313.pyc differ
 
backend/services/__pycache__/hf_registry.cpython-313.pyc CHANGED
Binary files a/backend/services/__pycache__/hf_registry.cpython-313.pyc and b/backend/services/__pycache__/hf_registry.cpython-313.pyc differ
 
backend/services/__pycache__/local_resource_service.cpython-313.pyc ADDED
Binary file (13.1 kB). View file
 
backend/services/auto_discovery_service.py CHANGED
@@ -91,7 +91,10 @@ class AutoDiscoveryService:
91
  if InferenceClient is None:
92
  logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
93
  else:
94
- hf_token = os.getenv("HF_API_TOKEN")
 
 
 
95
  try:
96
  self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
97
  logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
 
91
  if InferenceClient is None:
92
  logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
93
  else:
94
+ # Get HF token from environment or use default
95
+ from config import get_settings
96
+ settings = get_settings()
97
+ hf_token = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN") or settings.hf_token or "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"
98
  try:
99
  self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
100
  logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
backend/services/diagnostics_service.py CHANGED
@@ -260,7 +260,14 @@ class DiagnosticsService:
260
 
261
  try:
262
  from huggingface_hub import InferenceClient, HfApi
263
- api = HfApi()
 
 
 
 
 
 
 
264
 
265
  # بررسی مدل‌های استفاده شده
266
  models_to_check = [
 
260
 
261
  try:
262
  from huggingface_hub import InferenceClient, HfApi
263
+ import os
264
+ from config import get_settings
265
+
266
+ # Get HF token from settings or use default
267
+ settings = get_settings()
268
+ hf_token = settings.hf_token or os.getenv("HF_TOKEN") or "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"
269
+
270
+ api = HfApi(token=hf_token)
271
 
272
  # بررسی مدل‌های استفاده شده
273
  models_to_check = [
backend/services/hf_registry.py CHANGED
@@ -8,6 +8,16 @@ HF_API_DATASETS = "https://huggingface.co/api/datasets"
8
  REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
9
  HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
10
 
 
 
 
 
 
 
 
 
 
 
11
  # Curated Crypto Datasets
12
  CRYPTO_DATASETS = {
13
  "price": [
@@ -45,68 +55,81 @@ class HFRegistry:
45
  self.fail_reason: Optional[str] = None
46
 
47
  async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
48
- async with httpx.AsyncClient(timeout=HTTP_TIMEOUT) as client:
 
 
 
 
49
  r = await client.get(url, params=params)
50
  r.raise_for_status()
51
  return r.json()
52
 
53
  async def refresh(self) -> Dict[str, Any]:
 
 
 
 
54
  try:
55
- # Seed models
56
  for name in _SEED_MODELS:
57
  self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
58
 
59
- # Seed datasets with category metadata
60
  for category, dataset_list in CRYPTO_DATASETS.items():
61
  for name in dataset_list:
62
  self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
63
 
64
- # Fetch from HF Hub
65
- q_sent = {"pipeline_tag": "sentiment-analysis", "search": "crypto", "limit": 50}
66
- models = await self._hf_json(HF_API_MODELS, q_sent)
67
- for m in models or []:
68
- mid = m.get("modelId") or m.get("id") or m.get("name")
69
- if not mid: continue
70
- self.models[mid] = {
71
- "id": mid,
72
- "pipeline_tag": m.get("pipeline_tag"),
73
- "likes": m.get("likes"),
74
- "downloads": m.get("downloads"),
75
- "tags": m.get("tags") or [],
76
- "source": "hub"
77
- }
78
-
79
- q_crypto = {"search": "crypto", "limit": 100}
80
- datasets = await self._hf_json(HF_API_DATASETS, q_crypto)
81
- for d in datasets or []:
82
- did = d.get("id") or d.get("name")
83
- if not did: continue
84
- # Infer category from tags or name
85
- category = "other"
86
- tags_str = " ".join(d.get("tags") or []).lower()
87
- name_lower = did.lower()
88
- if "price" in tags_str or "ohlc" in tags_str or "price" in name_lower:
89
- category = "price"
90
- elif "news" in tags_str or "news" in name_lower:
91
- if "label" in tags_str or "sentiment" in tags_str:
92
- category = "news_labeled"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  else:
94
- category = "news_raw"
95
-
96
- self.datasets[did] = {
97
- "id": did,
98
- "likes": d.get("likes"),
99
- "downloads": d.get("downloads"),
100
- "tags": d.get("tags") or [],
101
- "category": category,
102
- "source": "hub"
103
- }
104
 
105
  self.last_refresh = time.time()
106
- self.fail_reason = None
107
- return {"ok": True, "models": len(self.models), "datasets": len(self.datasets)}
 
108
  except Exception as e:
109
- self.fail_reason = str(e)
110
  return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
111
 
112
  def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
 
8
  REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
9
  HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
10
 
11
+ HF_MODE = os.getenv("HF_MODE", "off").lower()
12
+ if HF_MODE not in ("off", "public", "auth"):
13
+ HF_MODE = "off"
14
+
15
+ HF_TOKEN = None
16
+ if HF_MODE == "auth":
17
+ HF_TOKEN = os.getenv("HF_TOKEN")
18
+ if not HF_TOKEN:
19
+ HF_MODE = "off"
20
+
21
  # Curated Crypto Datasets
22
  CRYPTO_DATASETS = {
23
  "price": [
 
55
  self.fail_reason: Optional[str] = None
56
 
57
  async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
58
+ headers = {}
59
+ if HF_MODE == "auth" and HF_TOKEN:
60
+ headers["Authorization"] = f"Bearer {HF_TOKEN}"
61
+
62
+ async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, headers=headers) as client:
63
  r = await client.get(url, params=params)
64
  r.raise_for_status()
65
  return r.json()
66
 
67
  async def refresh(self) -> Dict[str, Any]:
68
+ if HF_MODE == "off":
69
+ self.fail_reason = "HF_MODE=off"
70
+ return {"ok": False, "error": "HF_MODE=off", "models": 0, "datasets": 0}
71
+
72
  try:
 
73
  for name in _SEED_MODELS:
74
  self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
75
 
 
76
  for category, dataset_list in CRYPTO_DATASETS.items():
77
  for name in dataset_list:
78
  self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
79
 
80
+ if HF_MODE in ("public", "auth"):
81
+ try:
82
+ q_sent = {"pipeline_tag": "sentiment-analysis", "search": "crypto", "limit": 50}
83
+ models = await self._hf_json(HF_API_MODELS, q_sent)
84
+ for m in models or []:
85
+ mid = m.get("modelId") or m.get("id") or m.get("name")
86
+ if not mid: continue
87
+ self.models[mid] = {
88
+ "id": mid,
89
+ "pipeline_tag": m.get("pipeline_tag"),
90
+ "likes": m.get("likes"),
91
+ "downloads": m.get("downloads"),
92
+ "tags": m.get("tags") or [],
93
+ "source": "hub"
94
+ }
95
+
96
+ q_crypto = {"search": "crypto", "limit": 100}
97
+ datasets = await self._hf_json(HF_API_DATASETS, q_crypto)
98
+ for d in datasets or []:
99
+ did = d.get("id") or d.get("name")
100
+ if not did: continue
101
+ category = "other"
102
+ tags_str = " ".join(d.get("tags") or []).lower()
103
+ name_lower = did.lower()
104
+ if "price" in tags_str or "ohlc" in tags_str or "price" in name_lower:
105
+ category = "price"
106
+ elif "news" in tags_str or "news" in name_lower:
107
+ if "label" in tags_str or "sentiment" in tags_str:
108
+ category = "news_labeled"
109
+ else:
110
+ category = "news_raw"
111
+
112
+ self.datasets[did] = {
113
+ "id": did,
114
+ "likes": d.get("likes"),
115
+ "downloads": d.get("downloads"),
116
+ "tags": d.get("tags") or [],
117
+ "category": category,
118
+ "source": "hub"
119
+ }
120
+ except Exception as e:
121
+ error_msg = str(e)[:200]
122
+ if "401" in error_msg or "unauthorized" in error_msg.lower():
123
+ self.fail_reason = "Authentication failed"
124
  else:
125
+ self.fail_reason = error_msg
 
 
 
 
 
 
 
 
 
126
 
127
  self.last_refresh = time.time()
128
+ if self.fail_reason is None:
129
+ return {"ok": True, "models": len(self.models), "datasets": len(self.datasets)}
130
+ return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
131
  except Exception as e:
132
+ self.fail_reason = str(e)[:200]
133
  return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
134
 
135
  def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
backend/services/local_resource_service.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from copy import deepcopy
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ class LocalResourceService:
10
+ """Centralized loader for the unified fallback registry."""
11
+
12
+ def __init__(self, resource_path: Path):
13
+ self.resource_path = Path(resource_path)
14
+ self._raw_data: Optional[Dict[str, Any]] = None
15
+ self._assets: Dict[str, Dict[str, Any]] = {}
16
+ self._market_overview: Dict[str, Any] = {}
17
+ self._logger = logging.getLogger(__name__)
18
+
19
+ # --------------------------------------------------------------------- #
20
+ # Loading helpers
21
+ # --------------------------------------------------------------------- #
22
+ def _ensure_loaded(self) -> None:
23
+ if self._raw_data is not None:
24
+ return
25
+
26
+ try:
27
+ with self.resource_path.open("r", encoding="utf-8") as handle:
28
+ data = json.load(handle)
29
+ except FileNotFoundError:
30
+ self._logger.warning("Fallback registry %s not found", self.resource_path)
31
+ data = {}
32
+ except json.JSONDecodeError as exc:
33
+ self._logger.error("Invalid fallback registry JSON: %s", exc)
34
+ data = {}
35
+
36
+ fallback_data = data.get("fallback_data") or {}
37
+ assets = fallback_data.get("assets") or {}
38
+ normalized_assets: Dict[str, Dict[str, Any]] = {}
39
+
40
+ for key, details in assets.items():
41
+ symbol = str(details.get("symbol") or key).upper()
42
+ asset_copy = deepcopy(details)
43
+ asset_copy["symbol"] = symbol
44
+ normalized_assets[symbol] = asset_copy
45
+
46
+ self._raw_data = data
47
+ self._assets = normalized_assets
48
+ self._market_overview = deepcopy(fallback_data.get("market_overview") or {})
49
+
50
+ def refresh(self) -> None:
51
+ """Force reload from disk (used in tests)."""
52
+ self._raw_data = None
53
+ self._assets = {}
54
+ self._market_overview = {}
55
+ self._ensure_loaded()
56
+
57
+ # --------------------------------------------------------------------- #
58
+ # Registry level helpers
59
+ # --------------------------------------------------------------------- #
60
+ def get_registry(self) -> Dict[str, Any]:
61
+ self._ensure_loaded()
62
+ return deepcopy(self._raw_data or {})
63
+
64
+ def get_supported_symbols(self) -> List[str]:
65
+ self._ensure_loaded()
66
+ return sorted(self._assets.keys())
67
+
68
+ def has_fallback_data(self) -> bool:
69
+ self._ensure_loaded()
70
+ return bool(self._assets)
71
+
72
+ # --------------------------------------------------------------------- #
73
+ # Market data helpers
74
+ # --------------------------------------------------------------------- #
75
+ def _asset_to_market_record(self, asset: Dict[str, Any]) -> Dict[str, Any]:
76
+ price = asset.get("price", {})
77
+ return {
78
+ "id": asset.get("slug") or asset.get("symbol", "").lower(),
79
+ "symbol": asset.get("symbol"),
80
+ "name": asset.get("name"),
81
+ "current_price": price.get("current_price"),
82
+ "market_cap": price.get("market_cap"),
83
+ "market_cap_rank": asset.get("market_cap_rank"),
84
+ "total_volume": price.get("total_volume"),
85
+ "price_change_24h": price.get("price_change_24h"),
86
+ "price_change_percentage_24h": price.get("price_change_percentage_24h"),
87
+ "high_24h": price.get("high_24h"),
88
+ "low_24h": price.get("low_24h"),
89
+ "last_updated": price.get("last_updated"),
90
+ }
91
+
92
+ def get_top_prices(self, limit: int = 10) -> List[Dict[str, Any]]:
93
+ self._ensure_loaded()
94
+ if not self._assets:
95
+ return []
96
+
97
+ sorted_assets = sorted(
98
+ self._assets.values(),
99
+ key=lambda x: (x.get("market_cap_rank") or 9999, -(x.get("price", {}).get("market_cap") or 0)),
100
+ )
101
+ selected = sorted_assets[: max(1, limit)]
102
+ return [self._asset_to_market_record(asset) for asset in selected]
103
+
104
+ def get_prices_for_symbols(self, symbols: List[str]) -> List[Dict[str, Any]]:
105
+ self._ensure_loaded()
106
+ if not symbols or not self._assets:
107
+ return []
108
+
109
+ results: List[Dict[str, Any]] = []
110
+ for raw_symbol in symbols:
111
+ symbol = str(raw_symbol or "").upper()
112
+ asset = self._assets.get(symbol)
113
+ if asset:
114
+ results.append(self._asset_to_market_record(asset))
115
+ return results
116
+
117
+ def get_ticker_snapshot(self, symbol: str) -> Optional[Dict[str, Any]]:
118
+ self._ensure_loaded()
119
+ asset = self._assets.get(str(symbol or "").upper())
120
+ if not asset:
121
+ return None
122
+
123
+ price = asset.get("price", {})
124
+ return {
125
+ "symbol": asset.get("symbol"),
126
+ "price": price.get("current_price"),
127
+ "price_change_24h": price.get("price_change_24h"),
128
+ "price_change_percent_24h": price.get("price_change_percentage_24h"),
129
+ "high_24h": price.get("high_24h"),
130
+ "low_24h": price.get("low_24h"),
131
+ "volume_24h": price.get("total_volume"),
132
+ "quote_volume_24h": price.get("total_volume"),
133
+ }
134
+
135
+ def get_market_overview(self) -> Dict[str, Any]:
136
+ self._ensure_loaded()
137
+ if not self._assets:
138
+ return {}
139
+
140
+ overview = deepcopy(self._market_overview)
141
+ if not overview:
142
+ total_market_cap = sum(
143
+ (asset.get("price", {}) or {}).get("market_cap") or 0 for asset in self._assets.values()
144
+ )
145
+ total_volume = sum(
146
+ (asset.get("price", {}) or {}).get("total_volume") or 0 for asset in self._assets.values()
147
+ )
148
+ btc = self._assets.get("BTC", {})
149
+ btc_cap = (btc.get("price", {}) or {}).get("market_cap") or 0
150
+ overview = {
151
+ "total_market_cap": total_market_cap,
152
+ "total_volume_24h": total_volume,
153
+ "btc_dominance": (btc_cap / total_market_cap * 100) if total_market_cap else 0,
154
+ "active_cryptocurrencies": len(self._assets),
155
+ "markets": 500,
156
+ "market_cap_change_percentage_24h": 0,
157
+ }
158
+
159
+ # Enrich with derived leaderboards
160
+ gainers = sorted(
161
+ self._assets.values(),
162
+ key=lambda asset: (asset.get("price", {}) or {}).get("price_change_percentage_24h") or 0,
163
+ reverse=True,
164
+ )[:5]
165
+ losers = sorted(
166
+ self._assets.values(),
167
+ key=lambda asset: (asset.get("price", {}) or {}).get("price_change_percentage_24h") or 0,
168
+ )[:5]
169
+ volumes = sorted(
170
+ self._assets.values(),
171
+ key=lambda asset: (asset.get("price", {}) or {}).get("total_volume") or 0,
172
+ reverse=True,
173
+ )[:5]
174
+
175
+ overview["top_gainers"] = [self._asset_to_market_record(asset) for asset in gainers]
176
+ overview["top_losers"] = [self._asset_to_market_record(asset) for asset in losers]
177
+ overview["top_by_volume"] = [self._asset_to_market_record(asset) for asset in volumes]
178
+ overview["timestamp"] = overview.get("timestamp") or datetime.utcnow().isoformat()
179
+
180
+ return overview
181
+
182
+ def get_ohlcv(self, symbol: str, interval: str = "1h", limit: int = 100) -> List[Dict[str, Any]]:
183
+ self._ensure_loaded()
184
+ asset = self._assets.get(str(symbol or "").upper())
185
+ if not asset:
186
+ return []
187
+
188
+ ohlcv = (asset.get("ohlcv") or {}).get(interval) or []
189
+ if not ohlcv and interval != "1h":
190
+ # Provide 1h data for other intervals when nothing else is present
191
+ ohlcv = (asset.get("ohlcv") or {}).get("1h") or []
192
+
193
+ if limit and ohlcv:
194
+ return deepcopy(ohlcv[-limit:])
195
+ return deepcopy(ohlcv)
196
+
197
+ # --------------------------------------------------------------------- #
198
+ # Convenience helpers for testing / diagnostics
199
+ # --------------------------------------------------------------------- #
200
+ def describe(self) -> Dict[str, Any]:
201
+ """Simple snapshot used in diagnostics/tests."""
202
+ self._ensure_loaded()
203
+ return {
204
+ "resource_path": str(self.resource_path),
205
+ "assets": len(self._assets),
206
+ "supported_symbols": self.get_supported_symbols(),
207
+ }
check_server.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Check if the server is running and accessible
4
+ """
5
+ import sys
6
+ import socket
7
+ import requests
8
+ from pathlib import Path
9
+
10
+ def check_port(host='localhost', port=7860):
11
+ """Check if a port is open"""
12
+ try:
13
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
14
+ sock.settimeout(1)
15
+ result = sock.connect_ex((host, port))
16
+ sock.close()
17
+ return result == 0
18
+ except Exception as e:
19
+ print(f"Error checking port: {e}")
20
+ return False
21
+
22
+ def check_endpoint(url):
23
+ """Check if an endpoint is accessible"""
24
+ try:
25
+ response = requests.get(url, timeout=2)
26
+ return response.status_code, response.text[:100]
27
+ except requests.exceptions.ConnectionError:
28
+ return None, "Connection refused - server not running"
29
+ except Exception as e:
30
+ return None, str(e)
31
+
32
+ print("=" * 70)
33
+ print("Server Diagnostic Check")
34
+ print("=" * 70)
35
+
36
+ # Check if port is open
37
+ print("\n1. Checking if port 7860 is open...")
38
+ if check_port('localhost', 7860):
39
+ print(" ✓ Port 7860 is open")
40
+ else:
41
+ print(" ✗ Port 7860 is not open - server is NOT running")
42
+ print("\n SOLUTION: Start the server with:")
43
+ print(" python main.py")
44
+ sys.exit(1)
45
+
46
+ # Check if it's the correct server
47
+ print("\n2. Checking if correct server is running...")
48
+ status, text = check_endpoint('http://localhost:7860/health')
49
+ if status == 200:
50
+ print(" ✓ Health endpoint responds (correct server)")
51
+ elif status == 404:
52
+ print(" ✗ Health endpoint returns 404")
53
+ print(" WARNING: Something is running on port 7860, but it's not the FastAPI server!")
54
+ print(" This might be python -m http.server or another static file server.")
55
+ print("\n SOLUTION:")
56
+ print(" 1. Stop the current server (Ctrl+C in the terminal running it)")
57
+ print(" 2. Start the correct server: python main.py")
58
+ sys.exit(1)
59
+ else:
60
+ print(f" ✗ Health endpoint error: {status} - {text}")
61
+ sys.exit(1)
62
+
63
+ # Check API endpoints
64
+ print("\n3. Checking API endpoints...")
65
+ endpoints = [
66
+ '/api/market',
67
+ '/api/coins/top?limit=10',
68
+ '/api/news/latest?limit=20',
69
+ '/api/sentiment',
70
+ '/api/providers/config',
71
+ ]
72
+
73
+ all_ok = True
74
+ for endpoint in endpoints:
75
+ status, text = check_endpoint(f'http://localhost:7860{endpoint}')
76
+ if status == 200:
77
+ print(f" ✓ {endpoint}")
78
+ else:
79
+ print(f" ✗ {endpoint} - Status: {status}, Error: {text}")
80
+ all_ok = False
81
+
82
+ if not all_ok:
83
+ print("\n WARNING: Some endpoints are not working!")
84
+ print(" The server is running but routes may not be registered correctly.")
85
+ print(" Try restarting the server: python main.py")
86
+
87
+ # Check WebSocket (can't easily test, but check if route exists)
88
+ print("\n4. WebSocket endpoint:")
89
+ print(" The /ws endpoint should be available at ws://localhost:7860/ws")
90
+ print(" (Cannot test WebSocket from this script)")
91
+
92
+ print("\n" + "=" * 70)
93
+ if all_ok:
94
+ print("✓ Server is running correctly!")
95
+ print(" Access the dashboard at: http://localhost:7860/")
96
+ print(" API docs at: http://localhost:7860/docs")
97
+ else:
98
+ print("⚠ Server is running but some endpoints have issues")
99
+ print(" Try restarting: python main.py")
100
+ print("=" * 70)
101
+
collectors/__pycache__/aggregator.cpython-313.pyc CHANGED
Binary files a/collectors/__pycache__/aggregator.cpython-313.pyc and b/collectors/__pycache__/aggregator.cpython-313.pyc differ
 
collectors/aggregator.py CHANGED
@@ -88,6 +88,8 @@ class MarketDataCollector:
88
  self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
89
  self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
90
  self.timeout = 15.0
 
 
91
 
92
  async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
93
  provider = self.registry.providers.get(provider_key)
@@ -95,8 +97,54 @@ class MarketDataCollector:
95
  raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
96
 
97
  url = provider["base_url"].rstrip("/") + path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
99
  response = await client.get(url, params=params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  if response.status_code != 200:
101
  raise CollectorError(
102
  f"{provider_key} request failed with HTTP {response.status_code}",
@@ -105,14 +153,31 @@ class MarketDataCollector:
105
  )
106
  return response.json()
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
109
  cache_key = f"top_coins:{limit}"
110
  cached = await self.cache.get(cache_key)
111
  if cached:
112
  return cached
113
 
114
- providers = ["coingecko", "coincap"]
 
115
  last_error: Optional[Exception] = None
 
 
116
  for provider in providers:
117
  try:
118
  if provider == "coingecko":
@@ -160,11 +225,53 @@ class MarketDataCollector:
160
  ]
161
  await self.cache.set(cache_key, coins)
162
  return coins
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  except Exception as exc: # pragma: no cover - network heavy
164
  last_error = exc
165
- logger.warning("Provider %s failed: %s", provider, exc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
- raise CollectorError("Unable to fetch top coins", provider=str(last_error))
168
 
169
  async def _coin_id(self, symbol: str) -> str:
170
  symbol_lower = symbol.lower()
@@ -365,7 +472,9 @@ class ProviderStatusCollector:
365
  "latency_ms": latency,
366
  }
367
  except Exception as exc: # pragma: no cover - network heavy
368
- logger.warning("Provider %s health check failed: %s", provider_id, exc)
 
 
369
  return {
370
  "provider_id": provider_id,
371
  "name": data.get("name", provider_id),
 
88
  self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
89
  self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
90
  self.timeout = 15.0
91
+ self._last_error_log: Dict[str, float] = {} # Track last error log time per provider
92
+ self._error_log_throttle = 60.0 # Only log same error once per 60 seconds
93
 
94
  async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
95
  provider = self.registry.providers.get(provider_key)
 
97
  raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
98
 
99
  url = provider["base_url"].rstrip("/") + path
100
+
101
+ # Rate limit tracking per provider
102
+ if not hasattr(self, '_rate_limit_timestamps'):
103
+ self._rate_limit_timestamps: Dict[str, List[float]] = {}
104
+ if provider_key not in self._rate_limit_timestamps:
105
+ self._rate_limit_timestamps[provider_key] = []
106
+
107
+ # Get rate limits from provider config
108
+ rate_limit_rpm = provider.get("rate_limit", {}).get("requests_per_minute", 30)
109
+ if rate_limit_rpm and len(self._rate_limit_timestamps[provider_key]) >= rate_limit_rpm:
110
+ # Check if oldest request is older than 1 minute
111
+ oldest_time = self._rate_limit_timestamps[provider_key][0]
112
+ if time.time() - oldest_time < 60:
113
+ wait_time = 60 - (time.time() - oldest_time) + 1
114
+ if self._should_log_error(provider_key, "rate_limit_wait"):
115
+ logger.warning(f"Rate limiting {provider_key}, waiting {wait_time:.1f}s")
116
+ await asyncio.sleep(wait_time)
117
+ # Clean old timestamps
118
+ cutoff = time.time() - 60
119
+ self._rate_limit_timestamps[provider_key] = [
120
+ ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
121
+ ]
122
+
123
  async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
124
  response = await client.get(url, params=params)
125
+
126
+ # Record request timestamp
127
+ self._rate_limit_timestamps[provider_key].append(time.time())
128
+ # Keep only last minute of timestamps
129
+ cutoff = time.time() - 60
130
+ self._rate_limit_timestamps[provider_key] = [
131
+ ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
132
+ ]
133
+
134
+ # Handle HTTP 429 (Rate Limit) with exponential backoff
135
+ if response.status_code == 429:
136
+ retry_after = int(response.headers.get("Retry-After", "60"))
137
+ error_msg = f"{provider_key} rate limited (HTTP 429), retry after {retry_after}s"
138
+
139
+ if self._should_log_error(provider_key, "HTTP 429"):
140
+ logger.warning(error_msg)
141
+
142
+ raise CollectorError(
143
+ error_msg,
144
+ provider=provider_key,
145
+ status_code=429,
146
+ )
147
+
148
  if response.status_code != 200:
149
  raise CollectorError(
150
  f"{provider_key} request failed with HTTP {response.status_code}",
 
153
  )
154
  return response.json()
155
 
156
+ def _should_log_error(self, provider: str, error_msg: str) -> bool:
157
+ """Check if error should be logged (throttle repeated errors)."""
158
+ error_key = f"{provider}:{error_msg}"
159
+ now = time.time()
160
+ last_log_time = self._last_error_log.get(error_key, 0)
161
+
162
+ if now - last_log_time > self._error_log_throttle:
163
+ self._last_error_log[error_key] = now
164
+ # Clean up old entries (keep only last hour)
165
+ cutoff = now - 3600
166
+ self._last_error_log = {k: v for k, v in self._last_error_log.items() if v > cutoff}
167
+ return True
168
+ return False
169
+
170
  async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
171
  cache_key = f"top_coins:{limit}"
172
  cached = await self.cache.get(cache_key)
173
  if cached:
174
  return cached
175
 
176
+ # Provider list with priority order (add more fallbacks from resource files)
177
+ providers = ["coingecko", "coincap", "coinpaprika"]
178
  last_error: Optional[Exception] = None
179
+ last_error_details: Optional[str] = None
180
+
181
  for provider in providers:
182
  try:
183
  if provider == "coingecko":
 
225
  ]
226
  await self.cache.set(cache_key, coins)
227
  return coins
228
+
229
+ if provider == "coinpaprika":
230
+ data = await self._request("coinpaprika", "/tickers", {"quotes": "USD", "limit": limit})
231
+ coins = [
232
+ {
233
+ "name": item.get("name"),
234
+ "symbol": item.get("symbol", "").upper(),
235
+ "price": float(item.get("quotes", {}).get("USD", {}).get("price", 0)),
236
+ "change_24h": float(item.get("quotes", {}).get("USD", {}).get("percent_change_24h", 0)),
237
+ "market_cap": float(item.get("quotes", {}).get("USD", {}).get("market_cap", 0)),
238
+ "volume_24h": float(item.get("quotes", {}).get("USD", {}).get("volume_24h", 0)),
239
+ "rank": int(item.get("rank", 0)),
240
+ "last_updated": item.get("last_updated"),
241
+ }
242
+ for item in data[:limit] if item.get("quotes", {}).get("USD")
243
+ ]
244
+ await self.cache.set(cache_key, coins)
245
+ return coins
246
  except Exception as exc: # pragma: no cover - network heavy
247
  last_error = exc
248
+ error_msg = str(exc) if str(exc) else repr(exc)
249
+ error_type = type(exc).__name__
250
+
251
+ # Extract HTTP status code if available
252
+ if hasattr(exc, 'status_code'):
253
+ status_code = exc.status_code
254
+ error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
255
+ elif isinstance(exc, CollectorError) and hasattr(exc, 'status_code') and exc.status_code:
256
+ status_code = exc.status_code
257
+ error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
258
+
259
+ # Ensure we always have a meaningful error message
260
+ if not error_msg or error_msg.strip() == "":
261
+ error_msg = f"{error_type} (no details available)"
262
+
263
+ last_error_details = f"{error_type}: {error_msg}"
264
+
265
+ # Throttle error logging to prevent spam
266
+ error_key_for_logging = error_msg or error_type
267
+ if self._should_log_error(provider, error_key_for_logging):
268
+ logger.warning(
269
+ "Provider %s failed: %s (error logged, will suppress similar errors for 60s)",
270
+ provider,
271
+ last_error_details
272
+ )
273
 
274
+ raise CollectorError(f"Unable to fetch top coins from any provider. Last error: {last_error_details or 'Unknown'}", provider=str(last_error) if last_error else None)
275
 
276
  async def _coin_id(self, symbol: str) -> str:
277
  symbol_lower = symbol.lower()
 
472
  "latency_ms": latency,
473
  }
474
  except Exception as exc: # pragma: no cover - network heavy
475
+ error_msg = str(exc)
476
+ error_type = type(exc).__name__
477
+ logger.warning("Provider %s health check failed: %s: %s", provider_id, error_type, error_msg)
478
  return {
479
  "provider_id": provider_id,
480
  "name": data.get("name", provider_id),
config.js CHANGED
@@ -1,146 +1,389 @@
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
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Auto-detect localhost and use port 7860, otherwise use current origin
16
+ BACKEND_URL: (() => {
17
+ const hostname = window.location.hostname;
18
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
19
+ return `http://${hostname}:7860`;
20
  }
21
+ return window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
22
+ })(),
23
+ WS_URL: (() => {
24
+ const hostname = window.location.hostname;
25
+ let backendUrl;
26
+ if (hostname === 'localhost' || hostname === '127.0.0.1') {
27
+ backendUrl = `http://${hostname}:7860`;
28
+ } else {
29
+ backendUrl = window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
30
+ }
31
+ return backendUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws';
32
+ })(),
33
 
34
+ // ⏱️ Update Timing (milliseconds)
35
+ UPDATE_INTERVAL: 30000, // Every 30 seconds
36
+ CACHE_TTL: 60000, // 1 minute
37
+ HEARTBEAT_INTERVAL: 30000, // 30 seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ // 🔄 Reconnection Settings
40
+ MAX_RECONNECT_ATTEMPTS: 5,
41
+ RECONNECT_DELAY: 3000, // 3 seconds
42
+
43
+ // ═══════════════════════════════════════════════════════════════
44
+ // Display Settings
45
+ // ═══════════════════════════════════════════════════════════════
46
+
47
+ // Number of items to display
48
+ MAX_COINS_DISPLAY: 20, // Number of coins in table
49
+ MAX_NEWS_DISPLAY: 20, // Number of news items
50
+ MAX_TRENDING_DISPLAY: 10, // Number of trending items
51
+
52
+ // Table settings
53
+ TABLE_ROWS_PER_PAGE: 10,
54
+
55
+ // ═══════════════════════════════════════════════════════════════
56
+ // Chart Settings
57
+ // ═══════════════════════════════════════════════════════════════
58
+
59
+ CHART: {
60
+ DEFAULT_SYMBOL: 'BTCUSDT',
61
+ DEFAULT_INTERVAL: '1h',
62
+ AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
63
+ THEME: 'dark',
64
+ },
65
+
66
+ // ═══════════════════════════════════════════════════════════════
67
+ // AI Settings
68
+ // ═══════════════════════════════════════════════════════════════
69
+
70
+ AI: {
71
+ ENABLE_SENTIMENT: true,
72
+ ENABLE_NEWS_SUMMARY: true,
73
+ ENABLE_PRICE_PREDICTION: false, // Currently disabled
74
+ ENABLE_PATTERN_DETECTION: false, // Currently disabled
75
+ },
76
+
77
+ // ═══════════════════════════════════════════════════════════════
78
+ // Notification Settings
79
+ // ═══════════════════════════════════════════════════════════════
80
+
81
+ NOTIFICATIONS: {
82
+ ENABLE: true,
83
+ SHOW_PRICE_ALERTS: true,
84
+ SHOW_NEWS_ALERTS: true,
85
+ AUTO_DISMISS_TIME: 5000, // 5 seconds
86
+ },
87
+
88
+ // ═══════════════════════════════════════════════════════════════
89
+ // UI Settings
90
+ // ═══════════════════════════════════════════════════════════════
91
 
92
+ UI: {
93
+ DEFAULT_THEME: 'dark', // 'dark' or 'light'
94
+ ENABLE_ANIMATIONS: true,
95
+ ENABLE_SOUNDS: false,
96
+ LANGUAGE: 'en', // 'en' or 'fa'
97
+ RTL: false,
98
+ },
99
+
100
+ // ═══════════════════════════════════════════════════════════════
101
+ // Debug Settings
102
+ // ═══════════════════════════════════════════════════════════════
103
+
104
+ DEBUG: {
105
+ ENABLE_CONSOLE_LOGS: true,
106
+ ENABLE_PERFORMANCE_MONITORING: true,
107
+ SHOW_API_REQUESTS: true,
108
+ SHOW_WS_MESSAGES: false,
109
+ },
110
+
111
+ // ═══════════════════════════════════════════════════════════════
112
+ // Default Filters and Sorting
113
+ // ═══════════════════════════════════════════════════════════════
114
+
115
+ FILTERS: {
116
+ DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
117
+ DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
118
+ DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
119
+ SORT_ORDER: 'desc', // 'asc' or 'desc'
120
+ },
121
+
122
+ // ═══════════════════════════════════════════════════════════════
123
+ // HuggingFace Configuration
124
+ // ═══════════════════════════════════════════════════════════════
125
+
126
+ HF_TOKEN: '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',
127
+ HF_API_BASE: 'https://api-inference.huggingface.co/models',
128
+
129
+ // ═══════════════════════════════════════════════════════════════
130
+ // API Endpoints (Optional - if your backend differs)
131
+ // ═══════════════════════════════════════════════════════════════
132
+
133
+ ENDPOINTS: {
134
+ HEALTH: '/api/health',
135
+ MARKET: '/api/market/stats',
136
+ MARKET_PRICES: '/api/market/prices',
137
+ COINS_TOP: '/api/coins/top',
138
+ COIN_DETAILS: '/api/coins',
139
+ TRENDING: '/api/trending',
140
+ SENTIMENT: '/api/sentiment',
141
+ SENTIMENT_ANALYZE: '/api/sentiment/analyze',
142
+ NEWS: '/api/news/latest',
143
+ NEWS_SUMMARIZE: '/api/news/summarize',
144
+ STATS: '/api/stats',
145
+ PROVIDERS: '/api/providers',
146
+ PROVIDER_STATUS: '/api/providers/status',
147
+ CHART_HISTORY: '/api/charts/price',
148
+ CHART_ANALYZE: '/api/charts/analyze',
149
+ OHLCV: '/api/ohlcv',
150
+ QUERY: '/api/query',
151
+ DATASETS: '/api/datasets/list',
152
+ MODELS: '/api/models/list',
153
+ HF_HEALTH: '/api/hf/health',
154
+ HF_REGISTRY: '/api/hf/registry',
155
+ SYSTEM_STATUS: '/api/system/status',
156
+ SYSTEM_CONFIG: '/api/system/config',
157
+ CATEGORIES: '/api/categories',
158
+ RATE_LIMITS: '/api/rate-limits',
159
+ LOGS: '/api/logs',
160
+ ALERTS: '/api/alerts',
161
+ },
162
+
163
+ // ═══════════════════════════════════════════════════════════════
164
+ // WebSocket Events
165
+ // ═══════════════════════════════════════════════════════════════
166
+
167
+ WS_EVENTS: {
168
+ MARKET_UPDATE: 'market_update',
169
+ SENTIMENT_UPDATE: 'sentiment_update',
170
+ NEWS_UPDATE: 'news_update',
171
+ STATS_UPDATE: 'stats_update',
172
+ PRICE_UPDATE: 'price_update',
173
+ API_UPDATE: 'api_update',
174
+ STATUS_UPDATE: 'status_update',
175
+ SCHEDULE_UPDATE: 'schedule_update',
176
+ CONNECTED: 'connected',
177
+ DISCONNECTED: 'disconnected',
178
+ },
179
+
180
+ // ═══════════════════════════════════════════════════════════════
181
+ // Display Formats
182
+ // ═══════════════════════════════════════════════════════════════
183
+
184
+ FORMATS: {
185
+ CURRENCY: {
186
+ LOCALE: 'en-US',
187
+ STYLE: 'currency',
188
+ CURRENCY: 'USD',
189
  },
190
+ DATE: {
191
+ LOCALE: 'en-US',
192
+ OPTIONS: {
193
+ year: 'numeric',
194
+ month: 'long',
195
+ day: 'numeric',
196
+ hour: '2-digit',
197
+ minute: '2-digit',
198
+ },
199
+ },
200
+ },
201
+
202
+ // ═══════════════════════════════════════════════════════════════
203
+ // Rate Limiting
204
+ // ═══════════════════════════════════════════════════════════════
205
+
206
+ RATE_LIMITS: {
207
+ API_REQUESTS_PER_MINUTE: 60,
208
+ SEARCH_DEBOUNCE_MS: 300,
209
+ },
210
 
211
+ // ═══════════════════════════════════════════════════════════════
212
+ // Storage Settings
213
+ // ═══════════════════════════════════════════════════════════════
214
+
215
+ STORAGE: {
216
+ USE_LOCAL_STORAGE: true,
217
+ SAVE_PREFERENCES: true,
218
+ STORAGE_PREFIX: 'hts_dashboard_',
219
+ },
220
+ };
221
+
222
+ // ═══════════════════════════════════════════════════════════════════
223
+ // Predefined Profiles
224
+ // ═══════════════════════════════════════════════════════════════════
225
+
226
+ window.DASHBOARD_PROFILES = {
227
+
228
+ // High Performance Profile
229
+ HIGH_PERFORMANCE: {
230
+ UPDATE_INTERVAL: 15000, // Faster updates
231
+ CACHE_TTL: 30000, // Shorter cache
232
+ ENABLE_ANIMATIONS: false, // No animations
233
+ MAX_COINS_DISPLAY: 50,
234
+ },
235
+
236
+ // Data Saver Profile
237
+ DATA_SAVER: {
238
+ UPDATE_INTERVAL: 60000, // Less frequent updates
239
+ CACHE_TTL: 300000, // Longer cache (5 minutes)
240
+ MAX_COINS_DISPLAY: 10,
241
+ MAX_NEWS_DISPLAY: 10,
242
+ },
243
+
244
+ // Presentation Profile
245
+ PRESENTATION: {
246
+ ENABLE_ANIMATIONS: true,
247
+ UPDATE_INTERVAL: 20000,
248
+ SHOW_API_REQUESTS: false,
249
+ ENABLE_CONSOLE_LOGS: false,
250
+ },
251
+
252
+ // Development Profile
253
+ DEVELOPMENT: {
254
+ DEBUG: {
255
+ ENABLE_CONSOLE_LOGS: true,
256
+ ENABLE_PERFORMANCE_MONITORING: true,
257
+ SHOW_API_REQUESTS: true,
258
+ SHOW_WS_MESSAGES: true,
259
  },
260
+ UPDATE_INTERVAL: 10000,
261
+ },
262
+ };
263
+
264
+ // ═══════════════════════════════════════════════════════════════════
265
+ // Helper Function to Change Profile
266
+ // ═══════════════════════════════════════════════════════════════════
267
+
268
+ window.applyDashboardProfile = function (profileName) {
269
+ if (window.DASHBOARD_PROFILES[profileName]) {
270
+ const profile = window.DASHBOARD_PROFILES[profileName];
271
+ Object.assign(window.DASHBOARD_CONFIG, profile);
272
+ console.log(`✅ Profile "${profileName}" applied`);
273
+
274
+ // Reload application with new settings
275
+ if (window.app) {
276
+ window.app.destroy();
277
+ window.app = new DashboardApp();
278
+ window.app.init();
279
+ }
280
+ } else {
281
+ console.error(`❌ Profile "${profileName}" not found`);
282
+ }
283
+ };
284
+
285
+ // ═══════════════════════════════════════════════════════════════════
286
+ // Helper Function to Change Backend URL
287
+ // ═══════════════════════════════════════════════════════════════════
288
+
289
+ window.changeBackendURL = function (httpUrl, wsUrl) {
290
+ window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
291
+ window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
292
+
293
+ console.log('✅ Backend URL changed:');
294
+ console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
295
+ console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
296
+
297
+ // Reload application
298
+ if (window.app) {
299
+ window.app.destroy();
300
+ window.app = new DashboardApp();
301
+ window.app.init();
302
+ }
303
+ };
304
 
305
+ // ═══════════════════════════════════════════════════════════════════
306
+ // Save Settings to LocalStorage
307
+ // ═══════════════════════════════════════════════════════════════════
308
+
309
+ window.saveConfig = function () {
310
+ if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
311
+ try {
312
+ const configString = JSON.stringify(window.DASHBOARD_CONFIG);
313
+ localStorage.setItem(
314
+ window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
315
+ configString
316
+ );
317
+ console.log('✅ Settings saved');
318
+ } catch (error) {
319
+ console.error('❌ Error saving settings:', error);
320
+ }
321
+ }
322
+ };
323
+
324
+ // ═══════════════════════════════════════════════════════════════════
325
+ // Load Settings from LocalStorage
326
+ // ═══════════════════════════════════════════════════════════════════
327
+
328
+ window.loadConfig = function () {
329
+ if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
330
+ try {
331
+ const configString = localStorage.getItem(
332
+ window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config'
333
+ );
334
+ if (configString) {
335
+ const savedConfig = JSON.parse(configString);
336
+ Object.assign(window.DASHBOARD_CONFIG, savedConfig);
337
+ console.log('✅ Settings loaded');
338
  }
339
+ } catch (error) {
340
+ console.error('❌ Error loading settings:', error);
341
+ }
342
+ }
343
+ };
344
 
345
+ // ═══════════════════════════════════════════════════════════════════
346
+ // Auto-load Settings on Page Load
347
+ // ═══════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
348
 
349
+ if (document.readyState === 'loading') {
350
+ document.addEventListener('DOMContentLoaded', () => {
351
+ window.loadConfig();
352
+ });
353
+ } else {
354
+ window.loadConfig();
355
  }
356
 
357
+ // ═══════════════════════════════════════════════════════════════════
358
+ // Console Usage Guide
359
+ // ═══════════════════════════════════════════════════════════════════
360
+
361
+ console.log(`
362
+ ╔═══════════════════════════════════════════════════════════════╗
363
+ ║ HTS CRYPTO DASHBOARD - CONFIGURATION ║
364
+ ╚═══════════════════════════════════════════════════════════════╝
365
+
366
+ 📋 Available Commands:
367
+
368
+ 1. Change Profile:
369
+ applyDashboardProfile('HIGH_PERFORMANCE')
370
+ applyDashboardProfile('DATA_SAVER')
371
+ applyDashboardProfile('PRESENTATION')
372
+ applyDashboardProfile('DEVELOPMENT')
373
+
374
+ 2. Change Backend:
375
+ changeBackendURL('https://your-backend.com')
376
+
377
+ 3. Save/Load Settings:
378
+ saveConfig()
379
+ loadConfig()
380
+
381
+ 4. View Current Settings:
382
+ console.log(DASHBOARD_CONFIG)
383
+
384
+ 5. Manual Settings Change:
385
+ DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
386
+ saveConfig()
387
+
388
+ ═══════════════════════════════════════════════════════════════════
389
+ `);
config.py CHANGED
@@ -13,6 +13,13 @@ from pathlib import Path
13
  from typing import Dict, Any, List, Optional
14
  from dataclasses import dataclass
15
 
 
 
 
 
 
 
 
16
  # ==================== DIRECTORIES ====================
17
  BASE_DIR = Path(__file__).parent
18
  DATA_DIR = BASE_DIR / "data"
@@ -86,6 +93,9 @@ def get_settings() -> Settings:
86
  raw_token = os.environ.get("HF_TOKEN")
87
  encoded_token = os.environ.get("HF_TOKEN_ENCODED")
88
  decoded_token = raw_token or _decode_token(encoded_token)
 
 
 
89
 
90
  database_path = Path(os.environ.get("DATABASE_PATH", str(DB_DIR / "crypto_aggregator.db")))
91
 
 
13
  from typing import Dict, Any, List, Optional
14
  from dataclasses import dataclass
15
 
16
+ # Load .env file if python-dotenv is available
17
+ try:
18
+ from dotenv import load_dotenv
19
+ load_dotenv()
20
+ except ImportError:
21
+ pass # python-dotenv not installed, skip loading .env
22
+
23
  # ==================== DIRECTORIES ====================
24
  BASE_DIR = Path(__file__).parent
25
  DATA_DIR = BASE_DIR / "data"
 
93
  raw_token = os.environ.get("HF_TOKEN")
94
  encoded_token = os.environ.get("HF_TOKEN_ENCODED")
95
  decoded_token = raw_token or _decode_token(encoded_token)
96
+ # Default token if none provided
97
+ if not decoded_token:
98
+ decoded_token = "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"
99
 
100
  database_path = Path(os.environ.get("DATABASE_PATH", str(DB_DIR / "crypto_aggregator.db")))
101
 
crypto_resources_unified_2025-11-11.json CHANGED
The diff for this file is too large to render. See raw diff
 
enhanced_server.py CHANGED
@@ -199,9 +199,9 @@ from fastapi.responses import HTMLResponse, FileResponse
199
 
200
  @app.get("/", response_class=HTMLResponse)
201
  async def root():
202
- """Serve main dashboard"""
203
- if os.path.exists("index.html"):
204
- return FileResponse("index.html")
205
  else:
206
  return HTMLResponse("""
207
  <html>
 
199
 
200
  @app.get("/", response_class=HTMLResponse)
201
  async def root():
202
+ """Serve main admin dashboard"""
203
+ if os.path.exists("admin.html"):
204
+ return FileResponse("admin.html")
205
  else:
206
  return HTMLResponse("""
207
  <html>
hf_unified_server.py CHANGED
@@ -2,16 +2,38 @@
2
 
3
  import asyncio
4
  import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  from datetime import datetime, timedelta
6
- from fastapi import Body, FastAPI, HTTPException, Query
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
9
  from fastapi.staticfiles import StaticFiles
 
10
  from typing import Any, Dict, List, Optional, Union
 
11
  import logging
12
  import random
13
  import json
14
  from pathlib import Path
 
15
 
16
  from ai_models import (
17
  analyze_chart_points,
@@ -21,6 +43,7 @@ from ai_models import (
21
  initialize_models,
22
  registry_status,
23
  )
 
24
  from collectors.aggregator import (
25
  CollectorError,
26
  MarketDataCollector,
@@ -60,6 +83,7 @@ provider_collector = ProviderStatusCollector()
60
  # Load providers config
61
  WORKSPACE_ROOT = Path(__file__).parent
62
  PROVIDERS_CONFIG_PATH = settings.providers_config_path
 
63
 
64
  def load_providers_config():
65
  """Load providers from providers_config_extended.json"""
@@ -68,28 +92,67 @@ def load_providers_config():
68
  with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
69
  config = json.load(f)
70
  providers = config.get('providers', {})
71
- logger.info(f"Loaded {len(providers)} providers from providers_config_extended.json")
72
  return providers
73
  else:
74
- logger.warning(f"⚠️ providers_config_extended.json not found at {PROVIDERS_CONFIG_PATH}")
75
  return {}
76
  except Exception as e:
77
- logger.error(f"Error loading providers config: {e}")
78
  return {}
79
 
80
  # Load providers at startup
81
  PROVIDERS_CONFIG = load_providers_config()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  # Mount static files (CSS, JS)
84
  try:
85
  static_path = WORKSPACE_ROOT / "static"
86
  if static_path.exists():
87
  app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
88
- logger.info(f"Static files mounted from {static_path}")
89
  else:
90
- logger.warning(f"⚠️ Static directory not found: {static_path}")
91
  except Exception as e:
92
- logger.error(f"Error mounting static files: {e}")
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  # ============================================================================
95
  # Helper utilities & Data Fetching Functions
@@ -131,7 +194,11 @@ def _format_price_record(record: Dict[str, Any]) -> Dict[str, Any]:
131
 
132
 
133
  async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", limit: int = 100):
134
- """Fetch OHLCV data from Binance via the shared collector."""
 
 
 
 
135
 
136
  try:
137
  candles = await market_collector.get_ohlcv(symbol, interval, limit)
@@ -151,6 +218,7 @@ async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", lim
151
  async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int = 10):
152
  """Fetch price snapshots using the shared market collector."""
153
 
 
154
  try:
155
  if symbols:
156
  tasks = [market_collector.get_coin_details(_normalize_asset_symbol(sym)) for sym in symbols]
@@ -160,13 +228,17 @@ async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int
160
  if isinstance(result, Exception):
161
  continue
162
  coins.append(_format_price_record(result))
163
- return coins
164
-
165
- top = await market_collector.get_top_coins(limit=limit)
166
- return [_format_price_record(entry) for entry in top]
 
 
 
167
  except CollectorError as exc:
168
  logger.error("Error fetching aggregated prices: %s", exc)
169
- return []
 
170
 
171
 
172
  async def fetch_binance_ticker(symbol: str):
@@ -176,22 +248,29 @@ async def fetch_binance_ticker(symbol: str):
176
  coin = await market_collector.get_coin_details(_normalize_asset_symbol(symbol))
177
  except CollectorError as exc:
178
  logger.error("Unable to load ticker for %s: %s", symbol, exc)
179
- return None
180
-
181
- price = coin.get("price")
182
- change_pct = coin.get("change_24h") or 0.0
183
- change_abs = price * change_pct / 100 if price is not None and change_pct is not None else None
184
 
185
- return {
186
- "symbol": symbol.upper(),
187
- "price": price,
188
- "price_change_24h": change_abs,
189
- "price_change_percent_24h": change_pct,
190
- "high_24h": coin.get("high_24h"),
191
- "low_24h": coin.get("low_24h"),
192
- "volume_24h": coin.get("volume_24h"),
193
- "quote_volume_24h": coin.get("volume_24h"),
194
- }
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  # ============================================================================
@@ -300,6 +379,51 @@ async def get_providers():
300
  }
301
 
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  # ============================================================================
304
  # OHLCV Data Endpoint
305
  # ============================================================================
@@ -364,7 +488,7 @@ async def get_top_prices(limit: int = Query(10, ge=1, le=100, description="Numbe
364
  return {"data": cached_data, "source": "cache"}
365
 
366
  # Fetch from CoinGecko
367
- prices = await fetch_coingecko_prices(limit=limit)
368
 
369
  if prices:
370
  # Update cache
@@ -373,7 +497,7 @@ async def get_top_prices(limit: int = Query(10, ge=1, le=100, description="Numbe
373
  return {
374
  "count": len(prices),
375
  "data": prices,
376
- "source": "coingecko",
377
  "timestamp": datetime.now().isoformat()
378
  }
379
  else:
@@ -392,23 +516,23 @@ async def get_single_price(symbol: str):
392
  try:
393
  # Try Binance first for common pairs
394
  binance_symbol = f"{symbol.upper()}USDT"
395
- ticker = await fetch_binance_ticker(binance_symbol)
396
 
397
  if ticker:
398
  return {
399
  "symbol": symbol.upper(),
400
  "price": ticker,
401
- "source": "binance",
402
  "timestamp": datetime.now().isoformat()
403
  }
404
 
405
  # Fallback to CoinGecko
406
- prices = await fetch_coingecko_prices([symbol])
407
  if prices:
408
  return {
409
  "symbol": symbol.upper(),
410
  "price": prices[0],
411
- "source": "coingecko",
412
  "timestamp": datetime.now().isoformat()
413
  }
414
 
@@ -426,14 +550,51 @@ async def get_market_overview():
426
  """Get comprehensive market overview"""
427
  try:
428
  # Fetch top 20 coins
429
- prices = await fetch_coingecko_prices(limit=20)
430
 
431
  if not prices:
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(
@@ -454,7 +615,8 @@ async def get_market_overview():
454
  "top_gainers": gainers,
455
  "top_losers": losers,
456
  "top_by_volume": sorted(prices, key=lambda x: x.get("total_volume", 0) or 0, reverse=True)[:5],
457
- "timestamp": datetime.now().isoformat()
 
458
  }
459
 
460
  except HTTPException:
@@ -464,6 +626,59 @@ async def get_market_overview():
464
  raise HTTPException(status_code=500, detail=str(e))
465
 
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  @app.get("/api/market/prices")
468
  async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="Comma-separated symbols")):
469
  """Get prices for multiple cryptocurrencies"""
@@ -472,22 +687,29 @@ async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="C
472
 
473
  # Fetch prices
474
  prices_data = []
 
475
  for symbol in symbol_list:
476
  try:
477
- ticker = await fetch_binance_ticker(f"{symbol}USDT")
478
  if ticker:
479
  prices_data.append(ticker)
 
 
480
  except:
481
  continue
 
 
 
482
 
 
483
  if not prices_data:
484
- # Fallback to CoinGecko
485
- prices_data = await fetch_coingecko_prices(symbol_list)
486
 
487
  return {
488
  "symbols": symbol_list,
489
  "count": len(prices_data),
490
  "data": prices_data,
 
491
  "timestamp": datetime.now().isoformat()
492
  }
493
 
@@ -614,7 +836,7 @@ async def get_scoring_snapshot(symbol: str = Query("BTCUSDT", description="Tradi
614
  """Get comprehensive scoring snapshot"""
615
  try:
616
  # Fetch data
617
- ticker = await fetch_binance_ticker(symbol)
618
  ohlcv = await fetch_binance_ohlcv(symbol, "1h", 100)
619
 
620
  if not ticker or not ohlcv:
@@ -674,14 +896,25 @@ async def get_all_signals():
674
 
675
  @app.get("/api/sentiment")
676
  async def get_sentiment():
677
- """Get market sentiment data"""
 
 
 
 
 
678
  try:
679
  news = await news_collector.get_latest_news(limit=5)
680
  except CollectorError as exc:
681
- logger.warning("Sentiment fallback due to news error: %s", exc)
682
- news = []
 
 
 
 
 
 
 
683
 
684
- text = " ".join(item.get("title", "") for item in news).strip() or "Crypto market update"
685
  analysis = analyze_market_text(text)
686
  score = analysis.get("signals", {}).get("crypto", {}).get("score", 0.0)
687
  normalized_value = int((score + 1) * 50)
@@ -808,7 +1041,9 @@ async def get_alerts():
808
  @app.get("/api/hf/health")
809
  async def hf_health():
810
  """HuggingFace integration health"""
 
811
  status = registry_status()
 
812
  status["timestamp"] = datetime.utcnow().isoformat()
813
  return status
814
 
@@ -816,8 +1051,9 @@ async def hf_health():
816
  @app.post("/api/hf/refresh")
817
  async def hf_refresh():
818
  """Refresh HuggingFace data"""
 
819
  result = initialize_models()
820
- return {"status": "ok" if result.get("success") else "degraded", **result, "timestamp": datetime.utcnow().isoformat()}
821
 
822
 
823
  @app.get("/api/hf/registry")
@@ -827,6 +1063,83 @@ async def hf_registry(kind: str = "models"):
827
  return {"kind": kind, "items": info.get("model_names", info)}
828
 
829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
  def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dict[str, Any]:
831
  if isinstance(payload, list):
832
  return {"texts": payload, "mode": "auto"}
@@ -845,6 +1158,15 @@ def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dic
845
  @app.post("/api/hf/sentiment")
846
  async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
847
  """Run sentiment analysis using shared AI helpers."""
 
 
 
 
 
 
 
 
 
848
 
849
  try:
850
  resolved = _resolve_sentiment_payload(payload)
@@ -868,16 +1190,173 @@ async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
868
  return {"mode": mode, "results": results, "timestamp": datetime.utcnow().isoformat()}
869
 
870
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
  # ============================================================================
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)"""
878
- admin_path = WORKSPACE_ROOT / "admin.html"
879
- if admin_path.exists():
880
- return FileResponse(admin_path)
 
 
 
 
881
  return HTMLResponse("<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>")
882
 
883
  @app.get("/index.html", response_class=HTMLResponse)
@@ -898,12 +1377,26 @@ async def dashboard_alt():
898
  @app.get("/admin.html", response_class=HTMLResponse)
899
  async def admin():
900
  """Serve admin panel"""
901
- return FileResponse(WORKSPACE_ROOT / "admin.html")
 
 
 
 
 
 
 
902
 
903
  @app.get("/admin", response_class=HTMLResponse)
904
  async def admin_alt():
905
  """Alternative route for admin"""
906
- return FileResponse(WORKSPACE_ROOT / "admin.html")
 
 
 
 
 
 
 
907
 
908
  @app.get("/hf_console.html", response_class=HTMLResponse)
909
  async def hf_console():
@@ -944,43 +1437,281 @@ async def serve_html(filename: str):
944
  # Startup Event
945
  # ============================================================================
946
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  @app.on_event("startup")
948
  async def startup_event():
949
- """Initialize on startup"""
950
- # Initialize AI models
951
- from ai_models import initialize_models
952
- models_init = initialize_models()
953
- logger.info(f"✓ AI Models initialized: {models_init}")
954
-
955
- # Initialize HF Registry
956
- from backend.services.hf_registry import REGISTRY
957
- registry_result = await REGISTRY.refresh()
958
- logger.info(f"✓ HF Registry initialized: {registry_result}")
959
-
960
  logger.info("=" * 70)
961
- logger.info("🚀 Cryptocurrency Data & Analysis API Starting")
962
  logger.info("=" * 70)
963
- logger.info("FastAPI initialized")
964
- logger.info("CORS configured")
965
- logger.info("Cache initialized")
966
- logger.info(f"Providers loaded: {len(PROVIDERS_CONFIG)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
 
968
  # Show loaded HuggingFace Space providers
969
  hf_providers = [p for p in PROVIDERS_CONFIG.keys() if 'huggingface_space' in p]
970
  if hf_providers:
971
- logger.info(f"HuggingFace Space providers: {', '.join(hf_providers)}")
972
 
973
- logger.info("Data sources: Binance, CoinGecko, providers_config_extended.json")
974
 
975
  # Check HTML files
976
  html_files = ["index.html", "dashboard.html", "admin.html", "hf_console.html"]
977
  available_html = [f for f in html_files if (WORKSPACE_ROOT / f).exists()]
978
- logger.info(f"UI files: {len(available_html)}/{len(html_files)} available")
 
979
 
980
  logger.info("=" * 70)
981
- logger.info("📡 API ready at http://0.0.0.0:7860")
982
- logger.info("📖 Docs at http://0.0.0.0:7860/docs")
983
- logger.info("🎨 UI at http://0.0.0.0:7860/ (admin.html)")
984
  logger.info("=" * 70)
985
 
986
 
@@ -990,14 +1721,31 @@ async def startup_event():
990
 
991
  if __name__ == "__main__":
992
  import uvicorn
 
 
993
 
994
- print("=" * 70)
995
- print("🚀 Starting Cryptocurrency Data & Analysis API")
996
- print("=" * 70)
997
- print("📍 Server: http://localhost:7860")
998
- print("📖 API Docs: http://localhost:7860/docs")
999
- print("🔗 Health: http://localhost:7860/health")
1000
- print("=" * 70)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1001
 
1002
  uvicorn.run(
1003
  app,
@@ -1026,11 +1774,21 @@ class ConnectionManager:
1026
  logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
1027
 
1028
  async def broadcast(self, message: dict):
 
1029
  for connection in list(self.active_connections):
1030
  try:
1031
- await connection.send_json(message)
1032
- except:
1033
- self.disconnect(connection)
 
 
 
 
 
 
 
 
 
1034
 
1035
  ws_manager = ConnectionManager()
1036
 
@@ -1056,14 +1814,21 @@ async def get_top_coins(limit: int = Query(default=10, ge=1, le=100)):
1056
  result = []
1057
  for coin in coins:
1058
  result.append({
 
1059
  "rank": coin.get("rank", 0),
1060
  "symbol": coin.get("symbol", "").upper(),
1061
  "name": coin.get("name", ""),
1062
  "price": coin.get("price") or coin.get("current_price", 0),
 
1063
  "price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
 
 
1064
  "volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
 
1065
  "market_cap": coin.get("market_cap", 0),
1066
  "image": coin.get("image", ""),
 
 
1067
  "last_updated": coin.get("last_updated", datetime.now().isoformat())
1068
  })
1069
 
@@ -1111,14 +1876,26 @@ async def get_coin_detail(symbol: str):
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,
@@ -1175,6 +1952,12 @@ async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
1175
  return {"success": True, "news": [], "count": 0, "timestamp": datetime.now().isoformat()}
1176
 
1177
 
 
 
 
 
 
 
1178
  @app.post("/api/news/summarize")
1179
  async def summarize_news(item: Dict[str, Any] = Body(...)):
1180
  """Summarize a news article"""
@@ -1203,30 +1986,112 @@ async def summarize_news(item: Dict[str, Any] = Body(...)):
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,12 +2102,38 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
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,9 +2141,18 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
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 =====
@@ -1344,7 +2244,18 @@ async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=
1344
  # Attempt to load dataset
1345
  try:
1346
  from datasets import load_dataset
1347
- dataset = load_dataset(name, split="train", streaming=True)
 
 
 
 
 
 
 
 
 
 
 
1348
 
1349
  sample = []
1350
  for i, row in enumerate(dataset):
@@ -1429,6 +2340,11 @@ async def websocket_endpoint(websocket: WebSocket):
1429
 
1430
  try:
1431
  while True:
 
 
 
 
 
1432
  # Send market updates every 10 seconds
1433
  try:
1434
  # Get latest data
@@ -1451,16 +2367,42 @@ async def websocket_endpoint(websocket: WebSocket):
1451
  "timestamp": datetime.now().isoformat()
1452
  }
1453
 
1454
- await websocket.send_json({
1455
- "type": "update",
1456
- "payload": payload
1457
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1458
  except Exception as e:
1459
- logger.error(f"Error in WebSocket update: {e}")
 
 
 
 
 
 
1460
 
1461
  await asyncio.sleep(10)
1462
  except WebSocketDisconnect:
1463
- ws_manager.disconnect(websocket)
1464
  except Exception as e:
1465
  logger.error(f"WebSocket error: {e}")
1466
- ws_manager.disconnect(websocket)
 
 
 
 
 
2
 
3
  import asyncio
4
  import time
5
+ import os
6
+ import sys
7
+ import io
8
+
9
+ # Fix encoding for Windows console (must be done before any print/logging)
10
+ if sys.platform == "win32":
11
+ try:
12
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
13
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
14
+ except Exception:
15
+ pass # If already wrapped, ignore
16
+
17
+ # Set environment variables to force PyTorch and avoid TensorFlow/Keras issues
18
+ os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
19
+ os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
20
+ os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3') # Suppress TensorFlow warnings
21
+ # Force PyTorch as default framework
22
+ os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
23
+
24
  from datetime import datetime, timedelta
25
+ from fastapi import Body, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
26
  from fastapi.middleware.cors import CORSMiddleware
27
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
28
  from fastapi.staticfiles import StaticFiles
29
+ from starlette.websockets import WebSocketState
30
  from typing import Any, Dict, List, Optional, Union
31
+ from statistics import mean
32
  import logging
33
  import random
34
  import json
35
  from pathlib import Path
36
+ import httpx
37
 
38
  from ai_models import (
39
  analyze_chart_points,
 
43
  initialize_models,
44
  registry_status,
45
  )
46
+ from backend.services.local_resource_service import LocalResourceService
47
  from collectors.aggregator import (
48
  CollectorError,
49
  MarketDataCollector,
 
83
  # Load providers config
84
  WORKSPACE_ROOT = Path(__file__).parent
85
  PROVIDERS_CONFIG_PATH = settings.providers_config_path
86
+ FALLBACK_RESOURCE_PATH = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
87
 
88
  def load_providers_config():
89
  """Load providers from providers_config_extended.json"""
 
92
  with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
93
  config = json.load(f)
94
  providers = config.get('providers', {})
95
+ logger.info(f"Loaded {len(providers)} providers from providers_config_extended.json")
96
  return providers
97
  else:
98
+ logger.warning(f"providers_config_extended.json not found at {PROVIDERS_CONFIG_PATH}")
99
  return {}
100
  except Exception as e:
101
+ logger.error(f"Error loading providers config: {e}")
102
  return {}
103
 
104
  # Load providers at startup
105
  PROVIDERS_CONFIG = load_providers_config()
106
+ local_resource_service = LocalResourceService(FALLBACK_RESOURCE_PATH)
107
+
108
+ HF_SAMPLE_NEWS = [
109
+ {
110
+ "title": "Bitcoin holds key liquidity zone",
111
+ "source": "Fallback Ledger",
112
+ "sentiment": "positive",
113
+ "sentiment_score": 0.64,
114
+ "entities": ["BTC"],
115
+ "summary": "BTC consolidates near resistance with steady inflows",
116
+ },
117
+ {
118
+ "title": "Ethereum staking demand remains resilient",
119
+ "source": "Fallback Ledger",
120
+ "sentiment": "neutral",
121
+ "sentiment_score": 0.12,
122
+ "entities": ["ETH"],
123
+ "summary": "Validator queue shortens as fees stabilize around L2 adoption",
124
+ },
125
+ {
126
+ "title": "Solana ecosystem sees TVL uptick",
127
+ "source": "Fallback Ledger",
128
+ "sentiment": "positive",
129
+ "sentiment_score": 0.41,
130
+ "entities": ["SOL"],
131
+ "summary": "DeFi protocols move to Solana as mempool congestion drops",
132
+ },
133
+ ]
134
 
135
  # Mount static files (CSS, JS)
136
  try:
137
  static_path = WORKSPACE_ROOT / "static"
138
  if static_path.exists():
139
  app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
140
+ logger.info(f"Static files mounted from {static_path}")
141
  else:
142
+ logger.warning(f"Static directory not found: {static_path}")
143
  except Exception as e:
144
+ logger.error(f"Error mounting static files: {e}")
145
+
146
+ # Mount api-resources for frontend access
147
+ try:
148
+ api_resources_path = WORKSPACE_ROOT / "api-resources"
149
+ if api_resources_path.exists():
150
+ app.mount("/api-resources", StaticFiles(directory=str(api_resources_path)), name="api-resources")
151
+ logger.info(f"API resources mounted from {api_resources_path}")
152
+ else:
153
+ logger.warning(f"API resources directory not found: {api_resources_path}")
154
+ except Exception as e:
155
+ logger.error(f"Error mounting API resources: {e}")
156
 
157
  # ============================================================================
158
  # Helper utilities & Data Fetching Functions
 
194
 
195
 
196
  async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", limit: int = 100):
197
+ """Fetch OHLCV data from Binance via the shared collector.
198
+
199
+ If live data cannot be retrieved, this helper returns an empty list instead of
200
+ using any local/mock fallback so that upstream endpoints can fail honestly.
201
+ """
202
 
203
  try:
204
  candles = await market_collector.get_ohlcv(symbol, interval, limit)
 
218
  async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int = 10):
219
  """Fetch price snapshots using the shared market collector."""
220
 
221
+ source = "coingecko"
222
  try:
223
  if symbols:
224
  tasks = [market_collector.get_coin_details(_normalize_asset_symbol(sym)) for sym in symbols]
 
228
  if isinstance(result, Exception):
229
  continue
230
  coins.append(_format_price_record(result))
231
+ if coins:
232
+ return coins, source
233
+ else:
234
+ top = await market_collector.get_top_coins(limit=limit)
235
+ formatted = [_format_price_record(entry) for entry in top]
236
+ if formatted:
237
+ return formatted, source
238
  except CollectorError as exc:
239
  logger.error("Error fetching aggregated prices: %s", exc)
240
+ # No local/mock fallback – return an empty list so callers can signal real failure
241
+ return [], source
242
 
243
 
244
  async def fetch_binance_ticker(symbol: str):
 
248
  coin = await market_collector.get_coin_details(_normalize_asset_symbol(symbol))
249
  except CollectorError as exc:
250
  logger.error("Unable to load ticker for %s: %s", symbol, exc)
251
+ coin = None
 
 
 
 
252
 
253
+ if coin:
254
+ price = coin.get("price")
255
+ change_pct = coin.get("change_24h") or 0.0
256
+ change_abs = price * change_pct / 100 if price is not None and change_pct is not None else None
257
+ return {
258
+ "symbol": symbol.upper(),
259
+ "price": price,
260
+ "price_change_24h": change_abs,
261
+ "price_change_percent_24h": change_pct,
262
+ "high_24h": coin.get("high_24h"),
263
+ "low_24h": coin.get("low_24h"),
264
+ "volume_24h": coin.get("volume_24h"),
265
+ "quote_volume_24h": coin.get("volume_24h"),
266
+ }, "binance"
267
+
268
+ fallback_symbol = _normalize_asset_symbol(symbol)
269
+ fallback = local_resource_service.get_ticker_snapshot(fallback_symbol)
270
+ if fallback:
271
+ fallback["symbol"] = symbol.upper()
272
+ return fallback, "local-fallback"
273
+ return None, "binance"
274
 
275
 
276
  # ============================================================================
 
379
  }
380
 
381
 
382
+ @app.get("/api/providers/{provider_id}/health")
383
+ async def get_provider_health(provider_id: str):
384
+ """Get health status for a specific provider."""
385
+
386
+ # Check if provider exists in config
387
+ provider_config = PROVIDERS_CONFIG.get(provider_id)
388
+ if not provider_config:
389
+ raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
390
+
391
+ try:
392
+ # Perform health check using the collector
393
+ async with httpx.AsyncClient(timeout=provider_collector.timeout, headers=provider_collector.headers) as client:
394
+ health_result = await provider_collector._check_provider(client, provider_id, provider_config)
395
+
396
+ # Add metadata from config
397
+ health_result.update({
398
+ "base_url": provider_config.get("base_url"),
399
+ "requires_auth": provider_config.get("requires_auth"),
400
+ "priority": provider_config.get("priority"),
401
+ "category": provider_config.get("category"),
402
+ "last_checked": datetime.utcnow().isoformat()
403
+ })
404
+
405
+ return health_result
406
+ except Exception as exc: # pragma: no cover - network heavy
407
+ logger.error("Error checking provider health for %s: %s", provider_id, exc)
408
+ raise HTTPException(status_code=503, detail=f"Health check failed: {str(exc)}")
409
+
410
+
411
+ @app.get("/api/providers/config")
412
+ async def get_providers_config():
413
+ """Get providers configuration in format expected by frontend."""
414
+ try:
415
+ return {
416
+ "success": True,
417
+ "providers": PROVIDERS_CONFIG,
418
+ "total": len(PROVIDERS_CONFIG),
419
+ "source": str(PROVIDERS_CONFIG_PATH),
420
+ "last_updated": datetime.utcnow().isoformat()
421
+ }
422
+ except Exception as exc:
423
+ logger.error("Error getting providers config: %s", exc)
424
+ raise HTTPException(status_code=500, detail=str(exc))
425
+
426
+
427
  # ============================================================================
428
  # OHLCV Data Endpoint
429
  # ============================================================================
 
488
  return {"data": cached_data, "source": "cache"}
489
 
490
  # Fetch from CoinGecko
491
+ prices, source = await fetch_coingecko_prices(limit=limit)
492
 
493
  if prices:
494
  # Update cache
 
497
  return {
498
  "count": len(prices),
499
  "data": prices,
500
+ "source": source,
501
  "timestamp": datetime.now().isoformat()
502
  }
503
  else:
 
516
  try:
517
  # Try Binance first for common pairs
518
  binance_symbol = f"{symbol.upper()}USDT"
519
+ ticker, ticker_source = await fetch_binance_ticker(binance_symbol)
520
 
521
  if ticker:
522
  return {
523
  "symbol": symbol.upper(),
524
  "price": ticker,
525
+ "source": ticker_source,
526
  "timestamp": datetime.now().isoformat()
527
  }
528
 
529
  # Fallback to CoinGecko
530
+ prices, source = await fetch_coingecko_prices([symbol])
531
  if prices:
532
  return {
533
  "symbol": symbol.upper(),
534
  "price": prices[0],
535
+ "source": source,
536
  "timestamp": datetime.now().isoformat()
537
  }
538
 
 
550
  """Get comprehensive market overview"""
551
  try:
552
  # Fetch top 20 coins
553
+ prices, source = await fetch_coingecko_prices(limit=20)
554
 
555
  if not prices:
556
  raise HTTPException(status_code=503, detail="Unable to fetch market data")
557
 
558
  # Calculate market stats
559
+ # Try multiple field names for market cap and volume
560
+ total_market_cap = 0
561
+ total_volume = 0
562
+
563
+ for p in prices:
564
+ # Try different field names for market cap
565
+ market_cap = (
566
+ p.get("market_cap") or
567
+ p.get("market_cap_usd") or
568
+ p.get("market_cap_rank") or # Sometimes this is the value
569
+ None
570
+ )
571
+ # If market_cap is not found, try calculating from price and supply
572
+ if not market_cap:
573
+ price = p.get("price") or p.get("current_price") or 0
574
+ supply = p.get("circulating_supply") or p.get("total_supply") or 0
575
+ if price and supply:
576
+ market_cap = float(price) * float(supply)
577
+
578
+ if market_cap:
579
+ try:
580
+ total_market_cap += float(market_cap)
581
+ except (TypeError, ValueError):
582
+ pass
583
+
584
+ # Try different field names for volume
585
+ volume = (
586
+ p.get("total_volume") or
587
+ p.get("volume_24h") or
588
+ p.get("volume_24h_usd") or
589
+ None
590
+ )
591
+ if volume:
592
+ try:
593
+ total_volume += float(volume)
594
+ except (TypeError, ValueError):
595
+ pass
596
+
597
+ logger.info(f"Market overview: {len(prices)} coins, total_market_cap={total_market_cap:,.0f}, total_volume={total_volume:,.0f}")
598
 
599
  # Sort by 24h change
600
  gainers = sorted(
 
615
  "top_gainers": gainers,
616
  "top_losers": losers,
617
  "top_by_volume": sorted(prices, key=lambda x: x.get("total_volume", 0) or 0, reverse=True)[:5],
618
+ "timestamp": datetime.now().isoformat(),
619
+ "source": source
620
  }
621
 
622
  except HTTPException:
 
626
  raise HTTPException(status_code=500, detail=str(e))
627
 
628
 
629
+ @app.get("/api/market")
630
+ async def get_market():
631
+ """Get market data in format expected by frontend dashboard"""
632
+ try:
633
+ overview = await get_market_overview()
634
+ prices, source = await fetch_coingecko_prices(limit=50)
635
+
636
+ if not prices:
637
+ raise HTTPException(status_code=503, detail="Unable to fetch market data")
638
+
639
+ return {
640
+ "total_market_cap": overview.get("total_market_cap", 0),
641
+ "btc_dominance": overview.get("btc_dominance", 0),
642
+ "total_volume_24h": overview.get("total_volume_24h", 0),
643
+ "cryptocurrencies": prices,
644
+ "timestamp": datetime.now().isoformat(),
645
+ "source": source
646
+ }
647
+ except HTTPException:
648
+ raise
649
+ except Exception as e:
650
+ logger.error(f"Error in get_market: {e}")
651
+ raise HTTPException(status_code=500, detail=str(e))
652
+
653
+
654
+ @app.get("/api/trending")
655
+ async def get_trending():
656
+ """Get trending cryptocurrencies (top gainers by 24h change)"""
657
+ try:
658
+ prices, source = await fetch_coingecko_prices(limit=100)
659
+
660
+ if not prices:
661
+ raise HTTPException(status_code=503, detail="Unable to fetch trending data")
662
+
663
+ trending = sorted(
664
+ [p for p in prices if p.get("price_change_percentage_24h") is not None],
665
+ key=lambda x: x.get("price_change_percentage_24h", 0),
666
+ reverse=True
667
+ )[:10]
668
+
669
+ return {
670
+ "trending": trending,
671
+ "count": len(trending),
672
+ "timestamp": datetime.now().isoformat(),
673
+ "source": source
674
+ }
675
+ except HTTPException:
676
+ raise
677
+ except Exception as e:
678
+ logger.error(f"Error in get_trending: {e}")
679
+ raise HTTPException(status_code=500, detail=str(e))
680
+
681
+
682
  @app.get("/api/market/prices")
683
  async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="Comma-separated symbols")):
684
  """Get prices for multiple cryptocurrencies"""
 
687
 
688
  # Fetch prices
689
  prices_data = []
690
+ source = "binance"
691
  for symbol in symbol_list:
692
  try:
693
+ ticker, ticker_source = await fetch_binance_ticker(f"{symbol}USDT")
694
  if ticker:
695
  prices_data.append(ticker)
696
+ if ticker_source != "binance":
697
+ source = ticker_source
698
  except:
699
  continue
700
+ if not prices_data:
701
+ # Fallback to CoinGecko only – no local/mock data
702
+ prices_data, source = await fetch_coingecko_prices(symbol_list)
703
 
704
+ # If still empty, return an empty list with explicit source
705
  if not prices_data:
706
+ source = "none"
 
707
 
708
  return {
709
  "symbols": symbol_list,
710
  "count": len(prices_data),
711
  "data": prices_data,
712
+ "source": source,
713
  "timestamp": datetime.now().isoformat()
714
  }
715
 
 
836
  """Get comprehensive scoring snapshot"""
837
  try:
838
  # Fetch data
839
+ ticker, _ = await fetch_binance_ticker(symbol)
840
  ohlcv = await fetch_binance_ohlcv(symbol, "1h", 100)
841
 
842
  if not ticker or not ohlcv:
 
896
 
897
  @app.get("/api/sentiment")
898
  async def get_sentiment():
899
+ """Get market sentiment data.
900
+
901
+ This endpoint only returns a value when real news data is available.
902
+ If providers fail, we return an explicit HTTP 503 instead of synthesizing
903
+ sentiment from placeholder text.
904
+ """
905
  try:
906
  news = await news_collector.get_latest_news(limit=5)
907
  except CollectorError as exc:
908
+ logger.warning("Sentiment error due to news provider failure: %s", exc)
909
+ raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
910
+
911
+ if not news:
912
+ raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
913
+
914
+ text = " ".join(item.get("title", "") for item in news).strip()
915
+ if not text:
916
+ raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
917
 
 
918
  analysis = analyze_market_text(text)
919
  score = analysis.get("signals", {}).get("crypto", {}).get("score", 0.0)
920
  normalized_value = int((score + 1) * 50)
 
1041
  @app.get("/api/hf/health")
1042
  async def hf_health():
1043
  """HuggingFace integration health"""
1044
+ from ai_models import AI_MODELS_SUMMARY
1045
  status = registry_status()
1046
+ status["models"] = AI_MODELS_SUMMARY
1047
  status["timestamp"] = datetime.utcnow().isoformat()
1048
  return status
1049
 
 
1051
  @app.post("/api/hf/refresh")
1052
  async def hf_refresh():
1053
  """Refresh HuggingFace data"""
1054
+ from ai_models import initialize_models
1055
  result = initialize_models()
1056
+ return {"status": "ok" if result.get("models_loaded", 0) > 0 else "degraded", **result, "timestamp": datetime.utcnow().isoformat()}
1057
 
1058
 
1059
  @app.get("/api/hf/registry")
 
1063
  return {"kind": kind, "items": info.get("model_names", info)}
1064
 
1065
 
1066
+ @app.get("/api/resources/unified")
1067
+ async def get_unified_resources():
1068
+ """Get unified API resources from crypto_resources_unified_2025-11-11.json"""
1069
+ try:
1070
+ data = local_resource_service.get_registry()
1071
+ if data:
1072
+ metadata = data.get("registry", {}).get("metadata", {})
1073
+ return {
1074
+ "success": True,
1075
+ "data": data,
1076
+ "metadata": metadata,
1077
+ "count": metadata.get("total_entries", 0),
1078
+ "fallback_assets": len(local_resource_service.get_supported_symbols())
1079
+ }
1080
+ return {"success": False, "error": "Resources file not found"}
1081
+ except Exception as e:
1082
+ logger.error(f"Error loading unified resources: {e}")
1083
+ return {"success": False, "error": str(e)}
1084
+
1085
+
1086
+ @app.get("/api/resources/ultimate")
1087
+ async def get_ultimate_resources():
1088
+ """Get ultimate API resources from ultimate_crypto_pipeline_2025_NZasinich.json"""
1089
+ try:
1090
+ resources_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
1091
+ if resources_path.exists():
1092
+ with open(resources_path, 'r', encoding='utf-8') as f:
1093
+ data = json.load(f)
1094
+ return {
1095
+ "success": True,
1096
+ "data": data,
1097
+ "total_sources": data.get("total_sources", 0),
1098
+ "files": len(data.get("files", []))
1099
+ }
1100
+ return {"success": False, "error": "Resources file not found"}
1101
+ except Exception as e:
1102
+ logger.error(f"Error loading ultimate resources: {e}")
1103
+ return {"success": False, "error": str(e)}
1104
+
1105
+
1106
+ @app.get("/api/resources/stats")
1107
+ async def get_resources_stats():
1108
+ """Get statistics about available API resources"""
1109
+ try:
1110
+ stats = {
1111
+ "unified": {"available": False, "count": 0},
1112
+ "ultimate": {"available": False, "count": 0},
1113
+ "total_apis": 0
1114
+ }
1115
+
1116
+ # Check unified resources via the centralized loader
1117
+ registry = local_resource_service.get_registry()
1118
+ if registry:
1119
+ stats["unified"] = {
1120
+ "available": True,
1121
+ "count": registry.get("registry", {}).get("metadata", {}).get("total_entries", 0),
1122
+ "fallback_assets": len(local_resource_service.get_supported_symbols())
1123
+ }
1124
+
1125
+ # Check ultimate resources
1126
+ ultimate_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
1127
+ if ultimate_path.exists():
1128
+ with open(ultimate_path, 'r', encoding='utf-8') as f:
1129
+ ultimate_data = json.load(f)
1130
+ stats["ultimate"] = {
1131
+ "available": True,
1132
+ "count": ultimate_data.get("total_sources", 0)
1133
+ }
1134
+
1135
+ stats["total_apis"] = stats["unified"].get("count", 0) + stats["ultimate"].get("count", 0)
1136
+
1137
+ return {"success": True, "stats": stats}
1138
+ except Exception as e:
1139
+ logger.error(f"Error getting resources stats: {e}")
1140
+ return {"success": False, "error": str(e)}
1141
+
1142
+
1143
  def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dict[str, Any]:
1144
  if isinstance(payload, list):
1145
  return {"texts": payload, "mode": "auto"}
 
1158
  @app.post("/api/hf/sentiment")
1159
  async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
1160
  """Run sentiment analysis using shared AI helpers."""
1161
+ from ai_models import AI_MODELS_SUMMARY
1162
+
1163
+ if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
1164
+ return {
1165
+ "ok": False,
1166
+ "error": "No HF models are currently loaded.",
1167
+ "mode": AI_MODELS_SUMMARY.get("mode", "off"),
1168
+ "models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
1169
+ }
1170
 
1171
  try:
1172
  resolved = _resolve_sentiment_payload(payload)
 
1190
  return {"mode": mode, "results": results, "timestamp": datetime.utcnow().isoformat()}
1191
 
1192
 
1193
+ @app.post("/api/hf/models/sentiment")
1194
+ async def hf_models_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
1195
+ """Compatibility endpoint for HF console sentiment panel."""
1196
+ from ai_models import AI_MODELS_SUMMARY
1197
+
1198
+ if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
1199
+ return {
1200
+ "ok": False,
1201
+ "error": "No HF models are currently loaded.",
1202
+ "mode": AI_MODELS_SUMMARY.get("mode", "off"),
1203
+ "models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
1204
+ }
1205
+
1206
+ return await hf_sentiment(payload)
1207
+
1208
+
1209
+ @app.post("/api/hf/models/forecast")
1210
+ async def hf_models_forecast(payload: Dict[str, Any] = Body(...)):
1211
+ """Generate quick technical forecasts from provided closing prices."""
1212
+ series = payload.get("series") or payload.get("values") or payload.get("close")
1213
+ if not isinstance(series, list) or len(series) < 3:
1214
+ raise HTTPException(status_code=400, detail="Provide at least 3 closing prices in 'series'.")
1215
+
1216
+ try:
1217
+ floats = [float(x) for x in series]
1218
+ except (TypeError, ValueError) as exc:
1219
+ raise HTTPException(status_code=400, detail="Series must contain numeric values") from exc
1220
+
1221
+ model_name = (payload.get("model") or payload.get("model_name") or "btc_lstm").lower()
1222
+ steps = int(payload.get("steps") or 3)
1223
+
1224
+ deltas = [floats[i] - floats[i - 1] for i in range(1, len(floats))]
1225
+ avg_delta = mean(deltas)
1226
+ volatility = mean(abs(delta - avg_delta) for delta in deltas) if deltas else 0
1227
+
1228
+ predictions = []
1229
+ last = floats[-1]
1230
+ decay = 0.95 if model_name == "btc_arima" else 1.02
1231
+ for _ in range(steps):
1232
+ last = last + (avg_delta * decay)
1233
+ predictions.append(round(last, 4))
1234
+
1235
+ return {
1236
+ "model": model_name,
1237
+ "steps": steps,
1238
+ "input_count": len(floats),
1239
+ "volatility": round(volatility, 5),
1240
+ "predictions": predictions,
1241
+ "source": "local-fallback" if model_name == "btc_arima" else "hybrid",
1242
+ "timestamp": datetime.utcnow().isoformat()
1243
+ }
1244
+
1245
+
1246
+ @app.get("/api/hf/datasets/market/ohlcv")
1247
+ async def hf_dataset_market_ohlcv(symbol: str = Query("BTC"), interval: str = Query("1h"), limit: int = Query(120, ge=10, le=500)):
1248
+ """Expose fallback OHLCV snapshots as a pseudo HF dataset slice."""
1249
+ data = local_resource_service.get_ohlcv(symbol.upper(), interval, limit)
1250
+ source = "local-fallback"
1251
+
1252
+ if not data:
1253
+ return {
1254
+ "symbol": symbol.upper(),
1255
+ "interval": interval,
1256
+ "count": 0,
1257
+ "data": [],
1258
+ "source": source,
1259
+ "message": "No cached OHLCV available yet"
1260
+ }
1261
+
1262
+ return {
1263
+ "symbol": symbol.upper(),
1264
+ "interval": interval,
1265
+ "count": len(data),
1266
+ "data": data,
1267
+ "source": source,
1268
+ "timestamp": datetime.utcnow().isoformat()
1269
+ }
1270
+
1271
+
1272
+ @app.get("/api/hf/datasets/market/btc_technical")
1273
+ async def hf_dataset_market_btc(limit: int = Query(50, ge=10, le=200)):
1274
+ """Simplified technical metrics derived from fallback OHLCV data."""
1275
+ candles = local_resource_service.get_ohlcv("BTC", "1h", limit + 20)
1276
+
1277
+ if not candles:
1278
+ raise HTTPException(status_code=503, detail="Fallback OHLCV unavailable")
1279
+
1280
+ rows = []
1281
+ closes = [c["close"] for c in candles]
1282
+ for idx, candle in enumerate(candles[-limit:]):
1283
+ window = closes[max(0, idx): idx + 20]
1284
+ sma = sum(window) / len(window) if window else candle["close"]
1285
+ momentum = candle["close"] - candle["open"]
1286
+ rows.append({
1287
+ "timestamp": candle["timestamp"],
1288
+ "datetime": candle["datetime"],
1289
+ "close": candle["close"],
1290
+ "sma_20": round(sma, 4),
1291
+ "momentum": round(momentum, 4),
1292
+ "volatility": round((candle["high"] - candle["low"]) / candle["low"], 4)
1293
+ })
1294
+
1295
+ return {
1296
+ "symbol": "BTC",
1297
+ "interval": "1h",
1298
+ "count": len(rows),
1299
+ "items": rows,
1300
+ "source": "local-fallback"
1301
+ }
1302
+
1303
+
1304
+ @app.get("/api/hf/datasets/news/semantic")
1305
+ async def hf_dataset_news(limit: int = Query(10, ge=3, le=25)):
1306
+ """News slice augmented with sentiment tags for HF demos."""
1307
+ try:
1308
+ news = await news_collector.get_latest_news(limit=limit)
1309
+ source = "providers"
1310
+ except CollectorError:
1311
+ news = []
1312
+ source = "local-fallback"
1313
+
1314
+ if not news:
1315
+ # No mock dataset here – return an empty list when providers have no data
1316
+ items = []
1317
+ source = "none"
1318
+ else:
1319
+ items = []
1320
+ for item in news:
1321
+ items.append({
1322
+ "title": item.get("title"),
1323
+ "source": item.get("source") or item.get("provider"),
1324
+ "sentiment": item.get("sentiment") or "neutral",
1325
+ "sentiment_score": item.get("sentiment_confidence", 0.5),
1326
+ "entities": item.get("symbols") or [],
1327
+ "summary": item.get("summary") or item.get("description"),
1328
+ "published_at": item.get("date") or item.get("published_at")
1329
+ })
1330
+ return {
1331
+ "count": len(items),
1332
+ "items": items,
1333
+ "source": source,
1334
+ "timestamp": datetime.utcnow().isoformat()
1335
+ }
1336
+
1337
+
1338
  # ============================================================================
1339
  # HTML Routes - Serve UI files
1340
  # ============================================================================
1341
 
1342
+ @app.get("/favicon.ico")
1343
+ async def favicon():
1344
+ """Serve favicon"""
1345
+ favicon_path = WORKSPACE_ROOT / "static" / "favicon.ico"
1346
+ if favicon_path.exists():
1347
+ return FileResponse(favicon_path)
1348
+ return JSONResponse({"status": "no favicon"}, status_code=404)
1349
+
1350
  @app.get("/", response_class=HTMLResponse)
1351
  async def root():
1352
+ """Serve main HTML UI page (index.html)"""
1353
+ index_path = WORKSPACE_ROOT / "index.html"
1354
+ if index_path.exists():
1355
+ return FileResponse(
1356
+ path=str(index_path),
1357
+ media_type="text/html",
1358
+ filename="index.html"
1359
+ )
1360
  return HTMLResponse("<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>")
1361
 
1362
  @app.get("/index.html", response_class=HTMLResponse)
 
1377
  @app.get("/admin.html", response_class=HTMLResponse)
1378
  async def admin():
1379
  """Serve admin panel"""
1380
+ admin_path = WORKSPACE_ROOT / "admin.html"
1381
+ if admin_path.exists():
1382
+ return FileResponse(
1383
+ path=str(admin_path),
1384
+ media_type="text/html",
1385
+ filename="admin.html"
1386
+ )
1387
+ return HTMLResponse("<h1>Admin panel not found</h1>")
1388
 
1389
  @app.get("/admin", response_class=HTMLResponse)
1390
  async def admin_alt():
1391
  """Alternative route for admin"""
1392
+ admin_path = WORKSPACE_ROOT / "admin.html"
1393
+ if admin_path.exists():
1394
+ return FileResponse(
1395
+ path=str(admin_path),
1396
+ media_type="text/html",
1397
+ filename="admin.html"
1398
+ )
1399
+ return HTMLResponse("<h1>Admin panel not found</h1>")
1400
 
1401
  @app.get("/hf_console.html", response_class=HTMLResponse)
1402
  async def hf_console():
 
1437
  # Startup Event
1438
  # ============================================================================
1439
 
1440
+
1441
+ # ============================================================================
1442
+ # ADMIN DASHBOARD ENDPOINTS
1443
+ # ============================================================================
1444
+
1445
+ from fastapi import WebSocket, WebSocketDisconnect
1446
+ import asyncio
1447
+
1448
+ class ConnectionManager:
1449
+ def __init__(self):
1450
+ self.active_connections = []
1451
+ async def connect(self, websocket: WebSocket):
1452
+ await websocket.accept()
1453
+ self.active_connections.append(websocket)
1454
+ def disconnect(self, websocket: WebSocket):
1455
+ if websocket in self.active_connections:
1456
+ self.active_connections.remove(websocket)
1457
+ async def broadcast(self, message: dict):
1458
+ disconnected = []
1459
+ for conn in list(self.active_connections):
1460
+ try:
1461
+ # Check connection state before sending
1462
+ if conn.client_state == WebSocketState.CONNECTED:
1463
+ await conn.send_json(message)
1464
+ else:
1465
+ disconnected.append(conn)
1466
+ except Exception as e:
1467
+ logger.debug(f"Error broadcasting to client: {e}")
1468
+ disconnected.append(conn)
1469
+
1470
+ # Clean up disconnected clients
1471
+ for conn in disconnected:
1472
+ self.disconnect(conn)
1473
+
1474
+ ws_manager = ConnectionManager()
1475
+
1476
+ @app.get("/api/health")
1477
+ async def api_health():
1478
+ h = await health()
1479
+ return {"status": "healthy" if h.get("status") == "ok" else "degraded", **h}
1480
+
1481
+ # Removed duplicate - using improved version below
1482
+
1483
+ @app.get("/api/coins/{symbol}")
1484
+ async def get_coin_detail(symbol: str):
1485
+ coins = await market_collector.get_top_coins(limit=250)
1486
+ coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
1487
+ if not coin:
1488
+ raise HTTPException(404, f"Coin {symbol} not found")
1489
+ return {"success": True, "symbol": symbol.upper(), "name": coin.get("name", ""),
1490
+ "price": coin.get("price") or coin.get("current_price", 0),
1491
+ "change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1492
+ "market_cap": coin.get("market_cap", 0)}
1493
+
1494
+ @app.get("/api/market/stats")
1495
+ async def get_market_stats():
1496
+ """Get global market statistics (duplicate endpoint - keeping for compatibility)"""
1497
+ try:
1498
+ overview = await get_market_overview()
1499
+
1500
+ # Calculate ETH dominance from prices if available
1501
+ eth_dominance = 0
1502
+ if overview.get("total_market_cap", 0) > 0:
1503
+ try:
1504
+ eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1505
+ if eth_prices and len(eth_prices) > 0:
1506
+ eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1507
+ eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1508
+ except:
1509
+ pass
1510
+
1511
+ return {
1512
+ "success": True,
1513
+ "stats": {
1514
+ "total_market_cap": overview.get("total_market_cap", 0) or 0,
1515
+ "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1516
+ "btc_dominance": overview.get("btc_dominance", 0) or 0,
1517
+ "eth_dominance": eth_dominance,
1518
+ "active_cryptocurrencies": 10000,
1519
+ "markets": 500,
1520
+ "market_cap_change_24h": 0.0,
1521
+ "timestamp": datetime.now().isoformat()
1522
+ }
1523
+ }
1524
+ except Exception as e:
1525
+ logger.error(f"Error in /api/market/stats (duplicate): {e}")
1526
+ # No fake zeroed stats – surface a clear error instead
1527
+ raise HTTPException(status_code=503, detail="Market statistics are currently unavailable")
1528
+
1529
+
1530
+ @app.get("/api/stats")
1531
+ async def get_stats_alias():
1532
+ """Alias endpoint for /api/market/stats - backward compatibility"""
1533
+ return await get_market_stats()
1534
+
1535
+
1536
+ @app.get("/api/news/latest")
1537
+ async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
1538
+ from ai_models import analyze_news_item
1539
+ news = await news_collector.get_latest_news(limit=limit)
1540
+ enriched = []
1541
+ for item in news[:limit]:
1542
+ try:
1543
+ e = analyze_news_item(item)
1544
+ enriched.append({"title": e.get("title", ""), "source": e.get("source", ""),
1545
+ "published_at": e.get("published_at") or e.get("date", ""),
1546
+ "symbols": e.get("symbols", []), "sentiment": e.get("sentiment", "neutral"),
1547
+ "sentiment_confidence": e.get("sentiment_confidence", 0.5)})
1548
+ except:
1549
+ enriched.append({"title": item.get("title", ""), "source": item.get("source", ""),
1550
+ "published_at": item.get("date", ""), "symbols": item.get("symbols", []),
1551
+ "sentiment": "neutral", "sentiment_confidence": 0.5})
1552
+ return {"success": True, "news": enriched, "count": len(enriched)}
1553
+
1554
+ @app.post("/api/news/summarize")
1555
+ async def summarize_news(item: Dict[str, Any] = Body(...)):
1556
+ from ai_models import analyze_news_item
1557
+ e = analyze_news_item(item)
1558
+ return {"success": True, "summary": e.get("title", ""), "sentiment": e.get("sentiment", "neutral")}
1559
+
1560
+ # Duplicate endpoints removed - using the improved versions below in CHARTS ENDPOINTS section
1561
+
1562
+ @app.post("/api/sentiment/analyze")
1563
+ async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
1564
+ from ai_models import ensemble_crypto_sentiment
1565
+ result = ensemble_crypto_sentiment(payload.get("text", ""))
1566
+ return {"success": True, "sentiment": result["label"], "confidence": result["confidence"], "details": result}
1567
+
1568
+ @app.post("/api/query")
1569
+ async def process_query(payload: Dict[str, Any] = Body(...)):
1570
+ query = payload.get("query", "").lower()
1571
+ if "price" in query or "btc" in query:
1572
+ coins = await market_collector.get_top_coins(limit=10)
1573
+ btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
1574
+ if btc:
1575
+ return {"success": True, "type": "price", "message": f"Bitcoin is ${btc.get('price', 0):,.2f}", "data": btc}
1576
+ return {"success": True, "type": "general", "message": "Query processed"}
1577
+
1578
+ @app.get("/api/datasets/list")
1579
+ async def list_datasets():
1580
+ from backend.services.hf_registry import REGISTRY
1581
+ datasets = REGISTRY.list(kind="datasets")
1582
+ formatted = [{"name": d.get("id"), "category": d.get("category", "other"), "tags": d.get("tags", [])} for d in datasets]
1583
+ return {"success": True, "datasets": formatted, "count": len(formatted)}
1584
+
1585
+ @app.get("/api/datasets/sample")
1586
+ async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
1587
+ return {"success": False, "name": name, "sample": [], "message": "Auth required"}
1588
+
1589
+ @app.get("/api/models/list")
1590
+ async def list_models():
1591
+ from ai_models import get_model_info
1592
+ info = get_model_info()
1593
+ models = []
1594
+ for cat, mlist in info.get("model_catalog", {}).items():
1595
+ for mid in mlist:
1596
+ models.append({"name": mid, "task": "sentiment" if "sentiment" in cat else "analysis", "category": cat})
1597
+ return {"success": True, "models": models, "count": len(models)}
1598
+
1599
+ @app.post("/api/models/test")
1600
+ async def test_model(payload: Dict[str, Any] = Body(...)):
1601
+ from ai_models import ensemble_crypto_sentiment
1602
+ result = ensemble_crypto_sentiment(payload.get("text", ""))
1603
+ return {"success": True, "model": payload.get("model", ""), "result": result}
1604
+
1605
+ @app.websocket("/ws")
1606
+ async def websocket_endpoint(websocket: WebSocket):
1607
+ await ws_manager.connect(websocket)
1608
+ try:
1609
+ while True:
1610
+ # Check if connection is still open before sending
1611
+ if websocket.client_state != WebSocketState.CONNECTED:
1612
+ logger.info("WebSocket connection closed, breaking loop")
1613
+ break
1614
+
1615
+ try:
1616
+ top_coins = await market_collector.get_top_coins(limit=5)
1617
+ news = await news_collector.get_latest_news(limit=3)
1618
+ from ai_models import ensemble_crypto_sentiment
1619
+ sentiment = ensemble_crypto_sentiment(" ".join([n.get("title", "") for n in news])) if news else {"label": "neutral", "confidence": 0.5}
1620
+
1621
+ # Double-check connection state before sending
1622
+ if websocket.client_state == WebSocketState.CONNECTED:
1623
+ await websocket.send_json({
1624
+ "type": "update",
1625
+ "payload": {
1626
+ "market_data": top_coins,
1627
+ "news": news,
1628
+ "sentiment": sentiment,
1629
+ "timestamp": datetime.now().isoformat()
1630
+ }
1631
+ })
1632
+ else:
1633
+ logger.info("WebSocket disconnected, breaking loop")
1634
+ break
1635
+
1636
+ except CollectorError as e:
1637
+ # Provider errors are already logged by the collector, just continue
1638
+ logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
1639
+ # Use cached data if available, or empty data
1640
+ top_coins = []
1641
+ news = []
1642
+ sentiment = {"label": "neutral", "confidence": 0.5}
1643
+ except Exception as e:
1644
+ # Log other errors with full details
1645
+ error_msg = str(e) if str(e) else repr(e)
1646
+ logger.error(f"Error in WebSocket update loop: {type(e).__name__}: {error_msg}")
1647
+ # Don't break on data errors, just log and continue
1648
+ # Only break on connection errors
1649
+ if "send" in str(e).lower() or "close" in str(e).lower():
1650
+ break
1651
+
1652
+ await asyncio.sleep(10)
1653
+ except WebSocketDisconnect:
1654
+ logger.info("WebSocket disconnect exception caught")
1655
+ except Exception as e:
1656
+ logger.error(f"WebSocket endpoint error: {e}")
1657
+ finally:
1658
+ try:
1659
+ ws_manager.disconnect(websocket)
1660
+ except:
1661
+ pass
1662
+
1663
+
1664
  @app.on_event("startup")
1665
  async def startup_event():
1666
+ """Initialize on startup - non-blocking"""
 
 
 
 
 
 
 
 
 
 
1667
  logger.info("=" * 70)
1668
+ logger.info("Starting Cryptocurrency Data & Analysis API")
1669
  logger.info("=" * 70)
1670
+ logger.info("FastAPI initialized")
1671
+ logger.info("CORS configured")
1672
+ logger.info("Cache initialized")
1673
+ logger.info(f"Providers loaded: {len(PROVIDERS_CONFIG)}")
1674
+
1675
+ # Initialize AI models in background (non-blocking)
1676
+ async def init_models_background():
1677
+ try:
1678
+ from ai_models import initialize_models
1679
+ models_init = initialize_models()
1680
+ logger.info(f"AI Models initialized: {models_init}")
1681
+ except Exception as e:
1682
+ logger.warning(f"AI Models initialization failed: {e}")
1683
+
1684
+ # Initialize HF Registry in background (non-blocking)
1685
+ async def init_registry_background():
1686
+ try:
1687
+ from backend.services.hf_registry import REGISTRY
1688
+ registry_result = await REGISTRY.refresh()
1689
+ logger.info(f"HF Registry initialized: {registry_result}")
1690
+ except Exception as e:
1691
+ logger.warning(f"HF Registry initialization failed: {e}")
1692
+
1693
+ # Start background tasks
1694
+ asyncio.create_task(init_models_background())
1695
+ asyncio.create_task(init_registry_background())
1696
+ logger.info("Background initialization tasks started")
1697
 
1698
  # Show loaded HuggingFace Space providers
1699
  hf_providers = [p for p in PROVIDERS_CONFIG.keys() if 'huggingface_space' in p]
1700
  if hf_providers:
1701
+ logger.info(f"HuggingFace Space providers: {', '.join(hf_providers)}")
1702
 
1703
+ logger.info("Data sources: Binance, CoinGecko, providers_config_extended.json")
1704
 
1705
  # Check HTML files
1706
  html_files = ["index.html", "dashboard.html", "admin.html", "hf_console.html"]
1707
  available_html = [f for f in html_files if (WORKSPACE_ROOT / f).exists()]
1708
+ logger.info(f"UI files: {len(available_html)}/{len(html_files)} available")
1709
+ logger.info(f"HTML UI available at: http://0.0.0.0:7860/ (index.html)")
1710
 
1711
  logger.info("=" * 70)
1712
+ logger.info("API ready at http://0.0.0.0:7860")
1713
+ logger.info("Docs at http://0.0.0.0:7860/docs")
1714
+ logger.info("UI at http://0.0.0.0:7860/ (index.html - default HTML page)")
1715
  logger.info("=" * 70)
1716
 
1717
 
 
1721
 
1722
  if __name__ == "__main__":
1723
  import uvicorn
1724
+ import sys
1725
+ import io
1726
 
1727
+ # Fix encoding for Windows console
1728
+ if sys.platform == "win32":
1729
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
1730
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
1731
+
1732
+ try:
1733
+ print("=" * 70)
1734
+ print("Starting Cryptocurrency Data & Analysis API")
1735
+ print("=" * 70)
1736
+ print("Server: http://localhost:7860")
1737
+ print("API Docs: http://localhost:7860/docs")
1738
+ print("Health: http://localhost:7860/health")
1739
+ print("=" * 70)
1740
+ except UnicodeEncodeError:
1741
+ # Fallback if encoding still fails
1742
+ print("=" * 70)
1743
+ print("Starting Cryptocurrency Data & Analysis API")
1744
+ print("=" * 70)
1745
+ print("Server: http://localhost:7860")
1746
+ print("API Docs: http://localhost:7860/docs")
1747
+ print("Health: http://localhost:7860/health")
1748
+ print("=" * 70)
1749
 
1750
  uvicorn.run(
1751
  app,
 
1774
  logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
1775
 
1776
  async def broadcast(self, message: dict):
1777
+ disconnected = []
1778
  for connection in list(self.active_connections):
1779
  try:
1780
+ # Check connection state before sending
1781
+ if connection.client_state == WebSocketState.CONNECTED:
1782
+ await connection.send_json(message)
1783
+ else:
1784
+ disconnected.append(connection)
1785
+ except Exception as e:
1786
+ logger.debug(f"Error broadcasting to client: {e}")
1787
+ disconnected.append(connection)
1788
+
1789
+ # Clean up disconnected clients
1790
+ for connection in disconnected:
1791
+ self.disconnect(connection)
1792
 
1793
  ws_manager = ConnectionManager()
1794
 
 
1814
  result = []
1815
  for coin in coins:
1816
  result.append({
1817
+ "id": coin.get("id", coin.get("symbol", "").lower()),
1818
  "rank": coin.get("rank", 0),
1819
  "symbol": coin.get("symbol", "").upper(),
1820
  "name": coin.get("name", ""),
1821
  "price": coin.get("price") or coin.get("current_price", 0),
1822
+ "current_price": coin.get("price") or coin.get("current_price", 0),
1823
  "price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1824
+ "price_change_percentage_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
1825
+ "price_change_percentage_7d_in_currency": coin.get("price_change_percentage_7d", 0),
1826
  "volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
1827
+ "total_volume": coin.get("volume_24h") or coin.get("total_volume", 0),
1828
  "market_cap": coin.get("market_cap", 0),
1829
  "image": coin.get("image", ""),
1830
+ "sparkline_in_7d": coin.get("sparkline_in_7d") or {"price": []},
1831
+ "sparkline_data": coin.get("sparkline_data") or [],
1832
  "last_updated": coin.get("last_updated", datetime.now().isoformat())
1833
  })
1834
 
 
1876
  async def get_market_stats():
1877
  """Get global market statistics"""
1878
  try:
1879
+ # Use existing endpoint - get_market_overview returns total_market_cap and total_volume_24h
1880
  overview = await get_market_overview()
1881
 
1882
+ # Calculate ETH dominance from prices if available
1883
+ eth_dominance = 0
1884
+ if overview.get("total_market_cap", 0) > 0:
1885
+ # Try to get ETH market cap from top coins
1886
+ try:
1887
+ eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
1888
+ if eth_prices and len(eth_prices) > 0:
1889
+ eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
1890
+ eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
1891
+ except:
1892
+ pass
1893
+
1894
  stats = {
1895
+ "total_market_cap": overview.get("total_market_cap", 0) or 0,
1896
+ "total_volume_24h": overview.get("total_volume_24h", 0) or 0,
1897
+ "btc_dominance": overview.get("btc_dominance", 0) or 0,
1898
+ "eth_dominance": eth_dominance,
1899
  "active_cryptocurrencies": 10000, # Approximate
1900
  "markets": 500, # Approximate
1901
  "market_cap_change_24h": 0.0,
 
1952
  return {"success": True, "news": [], "count": 0, "timestamp": datetime.now().isoformat()}
1953
 
1954
 
1955
+ @app.get("/api/news")
1956
+ async def get_news(limit: int = Query(default=40, ge=1, le=100)):
1957
+ """Alias for /api/news/latest for backward compatibility"""
1958
+ return await get_latest_news(limit=limit)
1959
+
1960
+
1961
  @app.post("/api/news/summarize")
1962
  async def summarize_news(item: Dict[str, Any] = Body(...)):
1963
  """Summarize a news article"""
 
1986
  async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
1987
  """Get price chart data"""
1988
  try:
1989
+ # Clean and validate symbol
1990
+ symbol = symbol.strip().upper()
1991
+ if not symbol:
1992
+ return JSONResponse(
1993
+ status_code=400,
1994
+ content={
1995
+ "success": False,
1996
+ "symbol": "",
1997
+ "timeframe": timeframe,
1998
+ "data": [],
1999
+ "count": 0,
2000
+ "error": "Symbol cannot be empty"
2001
+ }
2002
+ )
2003
 
2004
+ logger.info(f"Fetching price history for {symbol} with timeframe {timeframe}")
2005
+
2006
+ # market_collector.get_price_history expects timeframe as string, not hours
2007
+ price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
2008
+
2009
+ if not price_history or len(price_history) == 0:
2010
+ logger.warning(f"No price history returned for {symbol}")
2011
+ return {
2012
+ "success": True,
2013
+ "symbol": symbol,
2014
+ "timeframe": timeframe,
2015
+ "data": [],
2016
+ "count": 0,
2017
+ "message": "No data available"
2018
+ }
2019
 
2020
  chart_data = []
2021
  for point in price_history:
2022
+ # Handle different timestamp formats
2023
+ timestamp = point.get("timestamp") or point.get("time") or point.get("date")
2024
+ price = point.get("price") or point.get("close") or point.get("value") or 0
2025
+
2026
+ # Convert timestamp to ISO format if needed
2027
+ if timestamp:
2028
+ try:
2029
+ # If it's already a string, use it
2030
+ if isinstance(timestamp, str):
2031
+ # Try to parse and format
2032
+ try:
2033
+ # Try ISO format first
2034
+ dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
2035
+ timestamp = dt.isoformat()
2036
+ except:
2037
+ try:
2038
+ # Try other common formats
2039
+ from dateutil import parser
2040
+ dt = parser.parse(timestamp)
2041
+ timestamp = dt.isoformat()
2042
+ except:
2043
+ pass
2044
+ elif isinstance(timestamp, (int, float)):
2045
+ # Unix timestamp
2046
+ dt = datetime.fromtimestamp(timestamp)
2047
+ timestamp = dt.isoformat()
2048
+ except Exception as e:
2049
+ logger.warning(f"Error parsing timestamp {timestamp}: {e}")
2050
+
2051
  chart_data.append({
2052
+ "timestamp": timestamp or "",
2053
+ "time": timestamp or "",
2054
+ "date": timestamp or "",
2055
+ "price": float(price) if price else 0,
2056
+ "close": float(price) if price else 0,
2057
+ "value": float(price) if price else 0
2058
  })
2059
 
2060
+ logger.info(f"Returning {len(chart_data)} data points for {symbol}")
2061
+
2062
  return {
2063
  "success": True,
2064
+ "symbol": symbol,
2065
  "timeframe": timeframe,
2066
  "data": chart_data,
2067
  "count": len(chart_data)
2068
  }
2069
+ except CollectorError as e:
2070
+ logger.error(f"Collector error in /api/charts/price/{symbol}: {e}", exc_info=True)
2071
+ return JSONResponse(
2072
+ status_code=200,
2073
+ content={
2074
+ "success": False,
2075
+ "symbol": symbol.upper() if symbol else "",
2076
+ "timeframe": timeframe,
2077
+ "data": [],
2078
+ "count": 0,
2079
+ "error": str(e)
2080
+ }
2081
+ )
2082
  except Exception as e:
2083
+ logger.error(f"Error in /api/charts/price/{symbol}: {e}", exc_info=True)
2084
+ return JSONResponse(
2085
+ status_code=200,
2086
+ content={
2087
+ "success": False,
2088
+ "symbol": symbol.upper() if symbol else "",
2089
+ "timeframe": timeframe,
2090
+ "data": [],
2091
+ "count": 0,
2092
+ "error": str(e)
2093
+ }
2094
+ )
2095
 
2096
 
2097
  @app.post("/api/charts/analyze")
 
2102
  timeframe = payload.get("timeframe", "7d")
2103
  indicators = payload.get("indicators", [])
2104
 
2105
+ if not symbol:
2106
+ return JSONResponse(
2107
+ status_code=400,
2108
+ content={"success": False, "error": "Symbol is required"}
2109
+ )
2110
+
2111
+ symbol = symbol.strip().upper()
2112
+ logger.info(f"Analyzing chart for {symbol} with timeframe {timeframe}")
2113
+
2114
+ # Get price data - use timeframe string, not hours
2115
+ price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
2116
+
2117
+ if not price_history or len(price_history) == 0:
2118
+ return {
2119
+ "success": False,
2120
+ "symbol": symbol,
2121
+ "timeframe": timeframe,
2122
+ "error": "No price data available for analysis"
2123
+ }
2124
 
2125
  # Analyze with AI
2126
  from ai_models import analyze_chart_points
2127
+ try:
2128
+ analysis = analyze_chart_points(price_history, indicators)
2129
+ except Exception as ai_error:
2130
+ logger.error(f"AI analysis error: {ai_error}", exc_info=True)
2131
+ # Return a basic analysis if AI fails
2132
+ analysis = {
2133
+ "direction": "neutral",
2134
+ "summary": "Analysis unavailable",
2135
+ "signals": []
2136
+ }
2137
 
2138
  return {
2139
  "success": True,
 
2141
  "timeframe": timeframe,
2142
  "analysis": analysis
2143
  }
2144
+ except CollectorError as e:
2145
+ logger.error(f"Collector error in /api/charts/analyze: {e}", exc_info=True)
2146
+ return JSONResponse(
2147
+ status_code=200,
2148
+ content={"success": False, "error": str(e)}
2149
+ )
2150
  except Exception as e:
2151
+ logger.error(f"Error in /api/charts/analyze: {e}", exc_info=True)
2152
+ return JSONResponse(
2153
+ status_code=200,
2154
+ content={"success": False, "error": str(e)}
2155
+ )
2156
 
2157
 
2158
  # ===== SENTIMENT ENDPOINTS =====
 
2244
  # Attempt to load dataset
2245
  try:
2246
  from datasets import load_dataset
2247
+ from config import get_settings
2248
+
2249
+ # Get HF token for dataset loading
2250
+ settings = get_settings()
2251
+ hf_token = settings.hf_token or "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"
2252
+
2253
+ # Set token in environment for datasets library
2254
+ import os
2255
+ if hf_token and not os.environ.get("HF_TOKEN"):
2256
+ os.environ["HF_TOKEN"] = hf_token
2257
+
2258
+ dataset = load_dataset(name, split="train", streaming=True, token=hf_token)
2259
 
2260
  sample = []
2261
  for i, row in enumerate(dataset):
 
2340
 
2341
  try:
2342
  while True:
2343
+ # Check if connection is still open before sending
2344
+ if websocket.client_state != WebSocketState.CONNECTED:
2345
+ logger.info("WebSocket connection closed, breaking loop")
2346
+ break
2347
+
2348
  # Send market updates every 10 seconds
2349
  try:
2350
  # Get latest data
 
2367
  "timestamp": datetime.now().isoformat()
2368
  }
2369
 
2370
+ # Double-check connection state before sending
2371
+ if websocket.client_state == WebSocketState.CONNECTED:
2372
+ await websocket.send_json({
2373
+ "type": "update",
2374
+ "payload": payload
2375
+ })
2376
+ else:
2377
+ logger.info("WebSocket disconnected, breaking loop")
2378
+ break
2379
+ except CollectorError as e:
2380
+ # Provider errors are already logged by the collector, just continue
2381
+ logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
2382
+ # Use empty data on provider errors
2383
+ payload = {
2384
+ "market_data": [],
2385
+ "stats": {"total_market_cap": 0, "sentiment": {"label": "neutral", "confidence": 0.5}},
2386
+ "news": [],
2387
+ "sentiment": {"label": "neutral", "confidence": 0.5},
2388
+ "timestamp": datetime.now().isoformat()
2389
+ }
2390
  except Exception as e:
2391
+ # Log other errors with full details
2392
+ error_msg = str(e) if str(e) else repr(e)
2393
+ logger.error(f"Error in WebSocket update: {type(e).__name__}: {error_msg}")
2394
+ # Don't break on data errors, just log and continue
2395
+ # Only break on connection errors
2396
+ if "send" in str(e).lower() or "close" in str(e).lower():
2397
+ break
2398
 
2399
  await asyncio.sleep(10)
2400
  except WebSocketDisconnect:
2401
+ logger.info("WebSocket disconnect exception caught")
2402
  except Exception as e:
2403
  logger.error(f"WebSocket error: {e}")
2404
+ finally:
2405
+ try:
2406
+ ws_manager.disconnect(websocket)
2407
+ except:
2408
+ pass
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
main.py CHANGED
@@ -1,31 +1,43 @@
1
  """
2
- Main entry point for HuggingFace Space
3
- Loads the unified API server with all endpoints
 
 
4
  """
5
  from pathlib import Path
6
  import sys
7
 
8
- # Add current directory to path
9
- current_dir = Path(__file__).resolve().parent
10
- sys.path.insert(0, str(current_dir))
 
 
 
11
 
12
- # Import the unified server app
13
  try:
14
- from hf_unified_server import app
15
- except ImportError as e:
16
- print(f"Error importing hf_unified_server: {e}")
17
- print("Falling back to basic app...")
18
- # Fallback to basic FastAPI app
 
19
  from fastapi import FastAPI
20
- app = FastAPI(title="Crypto API - Loading...")
21
-
 
22
  @app.get("/health")
23
  def health():
24
- return {"status": "loading", "message": "Server is starting up..."}
25
-
 
 
 
 
26
  @app.get("/")
27
  def root():
28
- return {"message": "Cryptocurrency Data API - Initializing..."}
 
 
 
29
 
30
- # Export app for uvicorn
31
  __all__ = ["app"]
 
1
  """
2
+ Main entry point for HuggingFace Space / local Uvicorn.
3
+ This file exposes `app` so that:
4
+ * Hugging Face Spaces can auto-discover it (`main:app`)
5
+ * You can run locally via: uvicorn main:app --host 0.0.0.0 --port 7860
6
  """
7
  from pathlib import Path
8
  import sys
9
 
10
+ # Ensure project root is importable
11
+ CURRENT_DIR = Path(__file__).resolve().parent
12
+ if str(CURRENT_DIR) not in sys.path:
13
+ sys.path.insert(0, str(CURRENT_DIR))
14
+
15
+ _import_error = None
16
 
 
17
  try:
18
+ # Canonical unified FastAPI backend with all endpoints used by index.html
19
+ from hf_unified_server import app # type: ignore
20
+ except Exception as e: # pragma: no cover - only used when something is really wrong
21
+ _import_error = e
22
+ # Minimal fallback app so deployment never hard-crashes,
23
+ # but clearly reports what went wrong.
24
  from fastapi import FastAPI
25
+
26
+ app = FastAPI(title="Crypto Intelligence Hub - Fallback")
27
+
28
  @app.get("/health")
29
  def health():
30
+ return {
31
+ "status": "error",
32
+ "message": "Failed to import hf_unified_server.app",
33
+ "detail": str(_import_error),
34
+ }
35
+
36
  @app.get("/")
37
  def root():
38
+ return {
39
+ "message": "Crypto backend failed to load. "
40
+ "Check logs and dependencies. See /health for details."
41
+ }
42
 
 
43
  __all__ = ["app"]
package-lock.json CHANGED
@@ -8,13 +8,390 @@
8
  "name": "crypto-api-resource-monitor",
9
  "version": "1.0.0",
10
  "license": "MIT",
 
 
 
11
  "devDependencies": {
12
- "fast-check": "^3.15.0"
 
13
  },
14
  "engines": {
15
  "node": ">=14.0.0"
16
  }
17
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  "node_modules/fast-check": {
19
  "version": "3.23.2",
20
  "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
@@ -38,6 +415,319 @@
38
  "node": ">=8.0.0"
39
  }
40
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  "node_modules/pure-rand": {
42
  "version": "6.1.0",
43
  "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
@@ -54,6 +744,223 @@
54
  }
55
  ],
56
  "license": "MIT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
58
  }
59
  }
 
8
  "name": "crypto-api-resource-monitor",
9
  "version": "1.0.0",
10
  "license": "MIT",
11
+ "dependencies": {
12
+ "charmap": "^1.1.6"
13
+ },
14
  "devDependencies": {
15
+ "fast-check": "^3.15.0",
16
+ "jsdom": "^23.0.0"
17
  },
18
  "engines": {
19
  "node": ">=14.0.0"
20
  }
21
  },
22
+ "node_modules/@asamuzakjp/css-color": {
23
+ "version": "3.2.0",
24
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
25
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
26
+ "dev": true,
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@csstools/css-calc": "^2.1.3",
30
+ "@csstools/css-color-parser": "^3.0.9",
31
+ "@csstools/css-parser-algorithms": "^3.0.4",
32
+ "@csstools/css-tokenizer": "^3.0.3",
33
+ "lru-cache": "^10.4.3"
34
+ }
35
+ },
36
+ "node_modules/@asamuzakjp/dom-selector": {
37
+ "version": "2.0.2",
38
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
39
+ "integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
40
+ "dev": true,
41
+ "license": "MIT",
42
+ "dependencies": {
43
+ "bidi-js": "^1.0.3",
44
+ "css-tree": "^2.3.1",
45
+ "is-potential-custom-element-name": "^1.0.1"
46
+ }
47
+ },
48
+ "node_modules/@csstools/color-helpers": {
49
+ "version": "5.1.0",
50
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
51
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
52
+ "dev": true,
53
+ "funding": [
54
+ {
55
+ "type": "github",
56
+ "url": "https://github.com/sponsors/csstools"
57
+ },
58
+ {
59
+ "type": "opencollective",
60
+ "url": "https://opencollective.com/csstools"
61
+ }
62
+ ],
63
+ "license": "MIT-0",
64
+ "engines": {
65
+ "node": ">=18"
66
+ }
67
+ },
68
+ "node_modules/@csstools/css-calc": {
69
+ "version": "2.1.4",
70
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
71
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
72
+ "dev": true,
73
+ "funding": [
74
+ {
75
+ "type": "github",
76
+ "url": "https://github.com/sponsors/csstools"
77
+ },
78
+ {
79
+ "type": "opencollective",
80
+ "url": "https://opencollective.com/csstools"
81
+ }
82
+ ],
83
+ "license": "MIT",
84
+ "engines": {
85
+ "node": ">=18"
86
+ },
87
+ "peerDependencies": {
88
+ "@csstools/css-parser-algorithms": "^3.0.5",
89
+ "@csstools/css-tokenizer": "^3.0.4"
90
+ }
91
+ },
92
+ "node_modules/@csstools/css-color-parser": {
93
+ "version": "3.1.0",
94
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
95
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
96
+ "dev": true,
97
+ "funding": [
98
+ {
99
+ "type": "github",
100
+ "url": "https://github.com/sponsors/csstools"
101
+ },
102
+ {
103
+ "type": "opencollective",
104
+ "url": "https://opencollective.com/csstools"
105
+ }
106
+ ],
107
+ "license": "MIT",
108
+ "dependencies": {
109
+ "@csstools/color-helpers": "^5.1.0",
110
+ "@csstools/css-calc": "^2.1.4"
111
+ },
112
+ "engines": {
113
+ "node": ">=18"
114
+ },
115
+ "peerDependencies": {
116
+ "@csstools/css-parser-algorithms": "^3.0.5",
117
+ "@csstools/css-tokenizer": "^3.0.4"
118
+ }
119
+ },
120
+ "node_modules/@csstools/css-parser-algorithms": {
121
+ "version": "3.0.5",
122
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
123
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
124
+ "dev": true,
125
+ "funding": [
126
+ {
127
+ "type": "github",
128
+ "url": "https://github.com/sponsors/csstools"
129
+ },
130
+ {
131
+ "type": "opencollective",
132
+ "url": "https://opencollective.com/csstools"
133
+ }
134
+ ],
135
+ "license": "MIT",
136
+ "peer": true,
137
+ "engines": {
138
+ "node": ">=18"
139
+ },
140
+ "peerDependencies": {
141
+ "@csstools/css-tokenizer": "^3.0.4"
142
+ }
143
+ },
144
+ "node_modules/@csstools/css-tokenizer": {
145
+ "version": "3.0.4",
146
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
147
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
148
+ "dev": true,
149
+ "funding": [
150
+ {
151
+ "type": "github",
152
+ "url": "https://github.com/sponsors/csstools"
153
+ },
154
+ {
155
+ "type": "opencollective",
156
+ "url": "https://opencollective.com/csstools"
157
+ }
158
+ ],
159
+ "license": "MIT",
160
+ "peer": true,
161
+ "engines": {
162
+ "node": ">=18"
163
+ }
164
+ },
165
+ "node_modules/agent-base": {
166
+ "version": "7.1.4",
167
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
168
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
169
+ "dev": true,
170
+ "license": "MIT",
171
+ "engines": {
172
+ "node": ">= 14"
173
+ }
174
+ },
175
+ "node_modules/asynckit": {
176
+ "version": "0.4.0",
177
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
178
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
179
+ "dev": true,
180
+ "license": "MIT"
181
+ },
182
+ "node_modules/bidi-js": {
183
+ "version": "1.0.3",
184
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
185
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
186
+ "dev": true,
187
+ "license": "MIT",
188
+ "dependencies": {
189
+ "require-from-string": "^2.0.2"
190
+ }
191
+ },
192
+ "node_modules/call-bind-apply-helpers": {
193
+ "version": "1.0.2",
194
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
195
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
196
+ "dev": true,
197
+ "license": "MIT",
198
+ "dependencies": {
199
+ "es-errors": "^1.3.0",
200
+ "function-bind": "^1.1.2"
201
+ },
202
+ "engines": {
203
+ "node": ">= 0.4"
204
+ }
205
+ },
206
+ "node_modules/charmap": {
207
+ "version": "1.1.6",
208
+ "resolved": "https://registry.npmjs.org/charmap/-/charmap-1.1.6.tgz",
209
+ "integrity": "sha512-BfgDyIZOETYrvthjHHLY44S3s21o/VRZoLBSbJbbMs/k2XluBvdayklV4BBs4tB0MgiUgAPRWoOkYeBLk58R1w==",
210
+ "license": "MIT",
211
+ "dependencies": {
212
+ "es6-object-assign": "^1.1.0"
213
+ }
214
+ },
215
+ "node_modules/combined-stream": {
216
+ "version": "1.0.8",
217
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
218
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
219
+ "dev": true,
220
+ "license": "MIT",
221
+ "dependencies": {
222
+ "delayed-stream": "~1.0.0"
223
+ },
224
+ "engines": {
225
+ "node": ">= 0.8"
226
+ }
227
+ },
228
+ "node_modules/css-tree": {
229
+ "version": "2.3.1",
230
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
231
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
232
+ "dev": true,
233
+ "license": "MIT",
234
+ "dependencies": {
235
+ "mdn-data": "2.0.30",
236
+ "source-map-js": "^1.0.1"
237
+ },
238
+ "engines": {
239
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
240
+ }
241
+ },
242
+ "node_modules/cssstyle": {
243
+ "version": "4.6.0",
244
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
245
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
246
+ "dev": true,
247
+ "license": "MIT",
248
+ "dependencies": {
249
+ "@asamuzakjp/css-color": "^3.2.0",
250
+ "rrweb-cssom": "^0.8.0"
251
+ },
252
+ "engines": {
253
+ "node": ">=18"
254
+ }
255
+ },
256
+ "node_modules/cssstyle/node_modules/rrweb-cssom": {
257
+ "version": "0.8.0",
258
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
259
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
260
+ "dev": true,
261
+ "license": "MIT"
262
+ },
263
+ "node_modules/data-urls": {
264
+ "version": "5.0.0",
265
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
266
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
267
+ "dev": true,
268
+ "license": "MIT",
269
+ "dependencies": {
270
+ "whatwg-mimetype": "^4.0.0",
271
+ "whatwg-url": "^14.0.0"
272
+ },
273
+ "engines": {
274
+ "node": ">=18"
275
+ }
276
+ },
277
+ "node_modules/debug": {
278
+ "version": "4.4.3",
279
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
280
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
281
+ "dev": true,
282
+ "license": "MIT",
283
+ "dependencies": {
284
+ "ms": "^2.1.3"
285
+ },
286
+ "engines": {
287
+ "node": ">=6.0"
288
+ },
289
+ "peerDependenciesMeta": {
290
+ "supports-color": {
291
+ "optional": true
292
+ }
293
+ }
294
+ },
295
+ "node_modules/decimal.js": {
296
+ "version": "10.6.0",
297
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
298
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
299
+ "dev": true,
300
+ "license": "MIT"
301
+ },
302
+ "node_modules/delayed-stream": {
303
+ "version": "1.0.0",
304
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
305
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
306
+ "dev": true,
307
+ "license": "MIT",
308
+ "engines": {
309
+ "node": ">=0.4.0"
310
+ }
311
+ },
312
+ "node_modules/dunder-proto": {
313
+ "version": "1.0.1",
314
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
315
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
316
+ "dev": true,
317
+ "license": "MIT",
318
+ "dependencies": {
319
+ "call-bind-apply-helpers": "^1.0.1",
320
+ "es-errors": "^1.3.0",
321
+ "gopd": "^1.2.0"
322
+ },
323
+ "engines": {
324
+ "node": ">= 0.4"
325
+ }
326
+ },
327
+ "node_modules/entities": {
328
+ "version": "6.0.1",
329
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
330
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
331
+ "dev": true,
332
+ "license": "BSD-2-Clause",
333
+ "engines": {
334
+ "node": ">=0.12"
335
+ },
336
+ "funding": {
337
+ "url": "https://github.com/fb55/entities?sponsor=1"
338
+ }
339
+ },
340
+ "node_modules/es-define-property": {
341
+ "version": "1.0.1",
342
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
343
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
344
+ "dev": true,
345
+ "license": "MIT",
346
+ "engines": {
347
+ "node": ">= 0.4"
348
+ }
349
+ },
350
+ "node_modules/es-errors": {
351
+ "version": "1.3.0",
352
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
353
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
354
+ "dev": true,
355
+ "license": "MIT",
356
+ "engines": {
357
+ "node": ">= 0.4"
358
+ }
359
+ },
360
+ "node_modules/es-object-atoms": {
361
+ "version": "1.1.1",
362
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
363
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
364
+ "dev": true,
365
+ "license": "MIT",
366
+ "dependencies": {
367
+ "es-errors": "^1.3.0"
368
+ },
369
+ "engines": {
370
+ "node": ">= 0.4"
371
+ }
372
+ },
373
+ "node_modules/es-set-tostringtag": {
374
+ "version": "2.1.0",
375
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
376
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
377
+ "dev": true,
378
+ "license": "MIT",
379
+ "dependencies": {
380
+ "es-errors": "^1.3.0",
381
+ "get-intrinsic": "^1.2.6",
382
+ "has-tostringtag": "^1.0.2",
383
+ "hasown": "^2.0.2"
384
+ },
385
+ "engines": {
386
+ "node": ">= 0.4"
387
+ }
388
+ },
389
+ "node_modules/es6-object-assign": {
390
+ "version": "1.1.0",
391
+ "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
392
+ "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==",
393
+ "license": "MIT"
394
+ },
395
  "node_modules/fast-check": {
396
  "version": "3.23.2",
397
  "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
 
415
  "node": ">=8.0.0"
416
  }
417
  },
418
+ "node_modules/form-data": {
419
+ "version": "4.0.5",
420
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
421
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
422
+ "dev": true,
423
+ "license": "MIT",
424
+ "dependencies": {
425
+ "asynckit": "^0.4.0",
426
+ "combined-stream": "^1.0.8",
427
+ "es-set-tostringtag": "^2.1.0",
428
+ "hasown": "^2.0.2",
429
+ "mime-types": "^2.1.12"
430
+ },
431
+ "engines": {
432
+ "node": ">= 6"
433
+ }
434
+ },
435
+ "node_modules/function-bind": {
436
+ "version": "1.1.2",
437
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
438
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
439
+ "dev": true,
440
+ "license": "MIT",
441
+ "funding": {
442
+ "url": "https://github.com/sponsors/ljharb"
443
+ }
444
+ },
445
+ "node_modules/get-intrinsic": {
446
+ "version": "1.3.0",
447
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
448
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
449
+ "dev": true,
450
+ "license": "MIT",
451
+ "dependencies": {
452
+ "call-bind-apply-helpers": "^1.0.2",
453
+ "es-define-property": "^1.0.1",
454
+ "es-errors": "^1.3.0",
455
+ "es-object-atoms": "^1.1.1",
456
+ "function-bind": "^1.1.2",
457
+ "get-proto": "^1.0.1",
458
+ "gopd": "^1.2.0",
459
+ "has-symbols": "^1.1.0",
460
+ "hasown": "^2.0.2",
461
+ "math-intrinsics": "^1.1.0"
462
+ },
463
+ "engines": {
464
+ "node": ">= 0.4"
465
+ },
466
+ "funding": {
467
+ "url": "https://github.com/sponsors/ljharb"
468
+ }
469
+ },
470
+ "node_modules/get-proto": {
471
+ "version": "1.0.1",
472
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
473
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
474
+ "dev": true,
475
+ "license": "MIT",
476
+ "dependencies": {
477
+ "dunder-proto": "^1.0.1",
478
+ "es-object-atoms": "^1.0.0"
479
+ },
480
+ "engines": {
481
+ "node": ">= 0.4"
482
+ }
483
+ },
484
+ "node_modules/gopd": {
485
+ "version": "1.2.0",
486
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
487
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
488
+ "dev": true,
489
+ "license": "MIT",
490
+ "engines": {
491
+ "node": ">= 0.4"
492
+ },
493
+ "funding": {
494
+ "url": "https://github.com/sponsors/ljharb"
495
+ }
496
+ },
497
+ "node_modules/has-symbols": {
498
+ "version": "1.1.0",
499
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
500
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
501
+ "dev": true,
502
+ "license": "MIT",
503
+ "engines": {
504
+ "node": ">= 0.4"
505
+ },
506
+ "funding": {
507
+ "url": "https://github.com/sponsors/ljharb"
508
+ }
509
+ },
510
+ "node_modules/has-tostringtag": {
511
+ "version": "1.0.2",
512
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
513
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
514
+ "dev": true,
515
+ "license": "MIT",
516
+ "dependencies": {
517
+ "has-symbols": "^1.0.3"
518
+ },
519
+ "engines": {
520
+ "node": ">= 0.4"
521
+ },
522
+ "funding": {
523
+ "url": "https://github.com/sponsors/ljharb"
524
+ }
525
+ },
526
+ "node_modules/hasown": {
527
+ "version": "2.0.2",
528
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
529
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
530
+ "dev": true,
531
+ "license": "MIT",
532
+ "dependencies": {
533
+ "function-bind": "^1.1.2"
534
+ },
535
+ "engines": {
536
+ "node": ">= 0.4"
537
+ }
538
+ },
539
+ "node_modules/html-encoding-sniffer": {
540
+ "version": "4.0.0",
541
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
542
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
543
+ "dev": true,
544
+ "license": "MIT",
545
+ "dependencies": {
546
+ "whatwg-encoding": "^3.1.1"
547
+ },
548
+ "engines": {
549
+ "node": ">=18"
550
+ }
551
+ },
552
+ "node_modules/http-proxy-agent": {
553
+ "version": "7.0.2",
554
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
555
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
556
+ "dev": true,
557
+ "license": "MIT",
558
+ "dependencies": {
559
+ "agent-base": "^7.1.0",
560
+ "debug": "^4.3.4"
561
+ },
562
+ "engines": {
563
+ "node": ">= 14"
564
+ }
565
+ },
566
+ "node_modules/https-proxy-agent": {
567
+ "version": "7.0.6",
568
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
569
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
570
+ "dev": true,
571
+ "license": "MIT",
572
+ "dependencies": {
573
+ "agent-base": "^7.1.2",
574
+ "debug": "4"
575
+ },
576
+ "engines": {
577
+ "node": ">= 14"
578
+ }
579
+ },
580
+ "node_modules/iconv-lite": {
581
+ "version": "0.6.3",
582
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
583
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
584
+ "dev": true,
585
+ "license": "MIT",
586
+ "dependencies": {
587
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
588
+ },
589
+ "engines": {
590
+ "node": ">=0.10.0"
591
+ }
592
+ },
593
+ "node_modules/is-potential-custom-element-name": {
594
+ "version": "1.0.1",
595
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
596
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
597
+ "dev": true,
598
+ "license": "MIT"
599
+ },
600
+ "node_modules/jsdom": {
601
+ "version": "23.2.0",
602
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
603
+ "integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
604
+ "dev": true,
605
+ "license": "MIT",
606
+ "dependencies": {
607
+ "@asamuzakjp/dom-selector": "^2.0.1",
608
+ "cssstyle": "^4.0.1",
609
+ "data-urls": "^5.0.0",
610
+ "decimal.js": "^10.4.3",
611
+ "form-data": "^4.0.0",
612
+ "html-encoding-sniffer": "^4.0.0",
613
+ "http-proxy-agent": "^7.0.0",
614
+ "https-proxy-agent": "^7.0.2",
615
+ "is-potential-custom-element-name": "^1.0.1",
616
+ "parse5": "^7.1.2",
617
+ "rrweb-cssom": "^0.6.0",
618
+ "saxes": "^6.0.0",
619
+ "symbol-tree": "^3.2.4",
620
+ "tough-cookie": "^4.1.3",
621
+ "w3c-xmlserializer": "^5.0.0",
622
+ "webidl-conversions": "^7.0.0",
623
+ "whatwg-encoding": "^3.1.1",
624
+ "whatwg-mimetype": "^4.0.0",
625
+ "whatwg-url": "^14.0.0",
626
+ "ws": "^8.16.0",
627
+ "xml-name-validator": "^5.0.0"
628
+ },
629
+ "engines": {
630
+ "node": ">=18"
631
+ },
632
+ "peerDependencies": {
633
+ "canvas": "^2.11.2"
634
+ },
635
+ "peerDependenciesMeta": {
636
+ "canvas": {
637
+ "optional": true
638
+ }
639
+ }
640
+ },
641
+ "node_modules/lru-cache": {
642
+ "version": "10.4.3",
643
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
644
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
645
+ "dev": true,
646
+ "license": "ISC"
647
+ },
648
+ "node_modules/math-intrinsics": {
649
+ "version": "1.1.0",
650
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
651
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
652
+ "dev": true,
653
+ "license": "MIT",
654
+ "engines": {
655
+ "node": ">= 0.4"
656
+ }
657
+ },
658
+ "node_modules/mdn-data": {
659
+ "version": "2.0.30",
660
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
661
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
662
+ "dev": true,
663
+ "license": "CC0-1.0"
664
+ },
665
+ "node_modules/mime-db": {
666
+ "version": "1.52.0",
667
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
668
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
669
+ "dev": true,
670
+ "license": "MIT",
671
+ "engines": {
672
+ "node": ">= 0.6"
673
+ }
674
+ },
675
+ "node_modules/mime-types": {
676
+ "version": "2.1.35",
677
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
678
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
679
+ "dev": true,
680
+ "license": "MIT",
681
+ "dependencies": {
682
+ "mime-db": "1.52.0"
683
+ },
684
+ "engines": {
685
+ "node": ">= 0.6"
686
+ }
687
+ },
688
+ "node_modules/ms": {
689
+ "version": "2.1.3",
690
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
691
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
692
+ "dev": true,
693
+ "license": "MIT"
694
+ },
695
+ "node_modules/parse5": {
696
+ "version": "7.3.0",
697
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
698
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "dependencies": {
702
+ "entities": "^6.0.0"
703
+ },
704
+ "funding": {
705
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
706
+ }
707
+ },
708
+ "node_modules/psl": {
709
+ "version": "1.15.0",
710
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
711
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
712
+ "dev": true,
713
+ "license": "MIT",
714
+ "dependencies": {
715
+ "punycode": "^2.3.1"
716
+ },
717
+ "funding": {
718
+ "url": "https://github.com/sponsors/lupomontero"
719
+ }
720
+ },
721
+ "node_modules/punycode": {
722
+ "version": "2.3.1",
723
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
724
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
725
+ "dev": true,
726
+ "license": "MIT",
727
+ "engines": {
728
+ "node": ">=6"
729
+ }
730
+ },
731
  "node_modules/pure-rand": {
732
  "version": "6.1.0",
733
  "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
 
744
  }
745
  ],
746
  "license": "MIT"
747
+ },
748
+ "node_modules/querystringify": {
749
+ "version": "2.2.0",
750
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
751
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
752
+ "dev": true,
753
+ "license": "MIT"
754
+ },
755
+ "node_modules/require-from-string": {
756
+ "version": "2.0.2",
757
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
758
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
759
+ "dev": true,
760
+ "license": "MIT",
761
+ "engines": {
762
+ "node": ">=0.10.0"
763
+ }
764
+ },
765
+ "node_modules/requires-port": {
766
+ "version": "1.0.0",
767
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
768
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
769
+ "dev": true,
770
+ "license": "MIT"
771
+ },
772
+ "node_modules/rrweb-cssom": {
773
+ "version": "0.6.0",
774
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
775
+ "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
776
+ "dev": true,
777
+ "license": "MIT"
778
+ },
779
+ "node_modules/safer-buffer": {
780
+ "version": "2.1.2",
781
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
782
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
783
+ "dev": true,
784
+ "license": "MIT"
785
+ },
786
+ "node_modules/saxes": {
787
+ "version": "6.0.0",
788
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
789
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
790
+ "dev": true,
791
+ "license": "ISC",
792
+ "dependencies": {
793
+ "xmlchars": "^2.2.0"
794
+ },
795
+ "engines": {
796
+ "node": ">=v12.22.7"
797
+ }
798
+ },
799
+ "node_modules/source-map-js": {
800
+ "version": "1.2.1",
801
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
802
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
803
+ "dev": true,
804
+ "license": "BSD-3-Clause",
805
+ "engines": {
806
+ "node": ">=0.10.0"
807
+ }
808
+ },
809
+ "node_modules/symbol-tree": {
810
+ "version": "3.2.4",
811
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
812
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
813
+ "dev": true,
814
+ "license": "MIT"
815
+ },
816
+ "node_modules/tough-cookie": {
817
+ "version": "4.1.4",
818
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
819
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
820
+ "dev": true,
821
+ "license": "BSD-3-Clause",
822
+ "dependencies": {
823
+ "psl": "^1.1.33",
824
+ "punycode": "^2.1.1",
825
+ "universalify": "^0.2.0",
826
+ "url-parse": "^1.5.3"
827
+ },
828
+ "engines": {
829
+ "node": ">=6"
830
+ }
831
+ },
832
+ "node_modules/tr46": {
833
+ "version": "5.1.1",
834
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
835
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
836
+ "dev": true,
837
+ "license": "MIT",
838
+ "dependencies": {
839
+ "punycode": "^2.3.1"
840
+ },
841
+ "engines": {
842
+ "node": ">=18"
843
+ }
844
+ },
845
+ "node_modules/universalify": {
846
+ "version": "0.2.0",
847
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
848
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
849
+ "dev": true,
850
+ "license": "MIT",
851
+ "engines": {
852
+ "node": ">= 4.0.0"
853
+ }
854
+ },
855
+ "node_modules/url-parse": {
856
+ "version": "1.5.10",
857
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
858
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
859
+ "dev": true,
860
+ "license": "MIT",
861
+ "dependencies": {
862
+ "querystringify": "^2.1.1",
863
+ "requires-port": "^1.0.0"
864
+ }
865
+ },
866
+ "node_modules/w3c-xmlserializer": {
867
+ "version": "5.0.0",
868
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
869
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
870
+ "dev": true,
871
+ "license": "MIT",
872
+ "dependencies": {
873
+ "xml-name-validator": "^5.0.0"
874
+ },
875
+ "engines": {
876
+ "node": ">=18"
877
+ }
878
+ },
879
+ "node_modules/webidl-conversions": {
880
+ "version": "7.0.0",
881
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
882
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
883
+ "dev": true,
884
+ "license": "BSD-2-Clause",
885
+ "engines": {
886
+ "node": ">=12"
887
+ }
888
+ },
889
+ "node_modules/whatwg-encoding": {
890
+ "version": "3.1.1",
891
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
892
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
893
+ "dev": true,
894
+ "license": "MIT",
895
+ "dependencies": {
896
+ "iconv-lite": "0.6.3"
897
+ },
898
+ "engines": {
899
+ "node": ">=18"
900
+ }
901
+ },
902
+ "node_modules/whatwg-mimetype": {
903
+ "version": "4.0.0",
904
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
905
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
906
+ "dev": true,
907
+ "license": "MIT",
908
+ "engines": {
909
+ "node": ">=18"
910
+ }
911
+ },
912
+ "node_modules/whatwg-url": {
913
+ "version": "14.2.0",
914
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
915
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
916
+ "dev": true,
917
+ "license": "MIT",
918
+ "dependencies": {
919
+ "tr46": "^5.1.0",
920
+ "webidl-conversions": "^7.0.0"
921
+ },
922
+ "engines": {
923
+ "node": ">=18"
924
+ }
925
+ },
926
+ "node_modules/ws": {
927
+ "version": "8.18.3",
928
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
929
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
930
+ "dev": true,
931
+ "license": "MIT",
932
+ "engines": {
933
+ "node": ">=10.0.0"
934
+ },
935
+ "peerDependencies": {
936
+ "bufferutil": "^4.0.1",
937
+ "utf-8-validate": ">=5.0.2"
938
+ },
939
+ "peerDependenciesMeta": {
940
+ "bufferutil": {
941
+ "optional": true
942
+ },
943
+ "utf-8-validate": {
944
+ "optional": true
945
+ }
946
+ }
947
+ },
948
+ "node_modules/xml-name-validator": {
949
+ "version": "5.0.0",
950
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
951
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
952
+ "dev": true,
953
+ "license": "Apache-2.0",
954
+ "engines": {
955
+ "node": ">=18"
956
+ }
957
+ },
958
+ "node_modules/xmlchars": {
959
+ "version": "2.2.0",
960
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
961
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
962
+ "dev": true,
963
+ "license": "MIT"
964
  }
965
  }
966
  }
package.json CHANGED
@@ -10,7 +10,16 @@
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",
@@ -32,5 +41,8 @@
32
  "repository": {
33
  "type": "git",
34
  "url": "https://github.com/nimazasinich/crypto-dt-source.git"
 
 
 
35
  }
36
  }
 
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
+ "test:ui-feedback": "node tests/test_ui_feedback.test.js",
17
+ "test:fallback": "pytest tests/test_fallback_service.py -m fallback",
18
+ "test:api-health": "pytest tests/test_fallback_service.py -m api_health"
19
+ },
20
+ "devDependencies": {
21
+ "fast-check": "^3.15.0",
22
+ "jsdom": "^23.0.0"
23
  },
24
  "keywords": [
25
  "cryptocurrency",
 
41
  "repository": {
42
  "type": "git",
43
  "url": "https://github.com/nimazasinich/crypto-dt-source.git"
44
+ },
45
+ "dependencies": {
46
+ "charmap": "^1.1.6"
47
  }
48
  }
production_server.py CHANGED
@@ -441,7 +441,7 @@ async def remove_custom_api(name: str):
441
  # Serve static files
442
  @app.get("/")
443
  async def root():
444
- return FileResponse("index.html")
445
 
446
  @app.get("/index.html")
447
  async def index():
 
441
  # Serve static files
442
  @app.get("/")
443
  async def root():
444
+ return FileResponse("admin.html")
445
 
446
  @app.get("/index.html")
447
  async def index():
provider_validator.py CHANGED
@@ -278,7 +278,13 @@ class ProviderValidator:
278
  try:
279
  start = time.time()
280
 
281
- async with httpx.AsyncClient(timeout=self.timeout) as client:
 
 
 
 
 
 
282
  response = await client.get(f"https://huggingface.co/api/models/{model_id}")
283
  elapsed_ms = (time.time() - start) * 1000
284
 
 
278
  try:
279
  start = time.time()
280
 
281
+ # Get HF token from environment or use default
282
+ hf_token = os.getenv("HF_TOKEN") or "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"
283
+ headers = {}
284
+ if hf_token:
285
+ headers["Authorization"] = f"Bearer {hf_token}"
286
+
287
+ async with httpx.AsyncClient(timeout=self.timeout, headers=headers) as client:
288
  response = await client.get(f"https://huggingface.co/api/models/{model_id}")
289
  elapsed_ms = (time.time() - start) * 1000
290
 
pytest.ini ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [pytest]
2
+ markers =
3
+ fallback: Tests validating the canonical fallback registry integration.
4
+ api_health: Tests covering API health/failover scenarios.
real_server.py CHANGED
@@ -380,7 +380,7 @@ async def api_config_keys():
380
  # Serve static files
381
  @app.get("/")
382
  async def root():
383
- return FileResponse("dashboard.html")
384
 
385
  @app.get("/dashboard.html")
386
  async def dashboard():
 
380
  # Serve static files
381
  @app.get("/")
382
  async def root():
383
+ return FileResponse("admin.html")
384
 
385
  @app.get("/dashboard.html")
386
  async def dashboard():