Upload 374 files
Browse files- QUICK_START.md +189 -46
- TEST_ENDPOINTS.sh +88 -161
- admin.html +994 -483
- config.js +130 -350
- hf_unified_server.py +22 -377
- index.html +1216 -765
- package.json +1 -6
- static/css/design-system.css +1 -1
- static/css/design-tokens.css +170 -292
- static/css/pro-dashboard.css +90 -2243
- static/js/aiAdvisorView.js +105 -70
- static/js/apiClient.js +17 -136
- static/js/apiExplorerView.js +2 -4
- static/js/app.js +76 -933
- static/js/chartLabView.js +88 -419
- static/js/datasetsModelsView.js +3 -9
- static/js/debugConsoleView.js +23 -25
- static/js/marketView.js +3 -16
- static/js/newsView.js +1 -3
- static/js/overviewView.js +64 -389
- static/js/providersView.js +0 -1
- static/js/uiUtils.js +2 -12
- static/js/wsClient.js +44 -297
- unified_dashboard.html +47 -190
QUICK_START.md
CHANGED
|
@@ -1,78 +1,221 @@
|
|
| 1 |
-
# 🚀
|
| 2 |
|
| 3 |
-
##
|
| 4 |
|
|
|
|
| 5 |
```bash
|
| 6 |
-
unzip crypto-hf-integrated-final.zip
|
| 7 |
-
cd crypto-dt-source-hf-integrated
|
| 8 |
-
python3 -m venv venv
|
| 9 |
-
source venv/bin/activate
|
| 10 |
pip install -r requirements.txt
|
| 11 |
-
uvicorn hf_unified_server:app --port 7860
|
| 12 |
```
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
```bash
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
```
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
|
|
|
| 29 |
```bash
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
```
|
| 32 |
|
| 33 |
-
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
4. **Sentiment & AI** - احساسات (10+ models)
|
| 39 |
-
5. **News** - اخبار با sentiment
|
| 40 |
-
6. **Providers** - 95 منابع
|
| 41 |
-
7. **API Explorer** - تست API
|
| 42 |
-
8. **Diagnostics** - سلامت سیستم
|
| 43 |
-
9. **Datasets & Models** - 14 dataset + 10 models
|
| 44 |
-
10. **Settings** - تنظیمات
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
##
|
| 56 |
|
| 57 |
-
**Port in use:**
|
| 58 |
```bash
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
```
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
```bash
|
| 64 |
-
|
|
|
|
| 65 |
```
|
| 66 |
|
| 67 |
-
|
| 68 |
```bash
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
```
|
| 71 |
|
| 72 |
-
## مستندات
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
| 75 |
-
- `DEPLOYMENT_GUIDE.md` - Production
|
| 76 |
-
- `ADMIN_HTML_INTEGRATION.md` - Frontend
|
| 77 |
|
| 78 |
-
|
|
|
|
| 1 |
+
# 🚀 راهنمای سریع شروع - Quick Start Guide
|
| 2 |
|
| 3 |
+
## ⚡ نصب و راهاندازی سریع
|
| 4 |
|
| 5 |
+
### 1️⃣ نصب وابستگیها
|
| 6 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
pip install -r requirements.txt
|
|
|
|
| 8 |
```
|
| 9 |
|
| 10 |
+
### 2️⃣ Import منابع از فایلهای JSON
|
| 11 |
+
```bash
|
| 12 |
+
python import_resources.py
|
| 13 |
+
```
|
| 14 |
+
این اسکریپت بهطور خودکار همه منابع را از فایلهای JSON موجود import میکند.
|
| 15 |
+
|
| 16 |
+
### 3️⃣ راهاندازی سرور
|
| 17 |
+
```bash
|
| 18 |
+
# روش 1: استفاده از اسکریپت راهانداز
|
| 19 |
+
python start_server.py
|
| 20 |
+
|
| 21 |
+
# روش 2: مستقیم
|
| 22 |
+
python api_server_extended.py
|
| 23 |
|
| 24 |
+
# روش 3: با uvicorn
|
| 25 |
+
uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
### 4️⃣ دسترسی به داشبورد
|
| 29 |
+
```
|
| 30 |
+
http://localhost:8000
|
| 31 |
+
```
|
| 32 |
|
| 33 |
+
## 📋 تبهای داشبورد
|
| 34 |
+
|
| 35 |
+
### 📊 Market
|
| 36 |
+
- آمار کلی بازار
|
| 37 |
+
- لیست کریپتوکارنسیها
|
| 38 |
+
- نمودارها و ترندینگ
|
| 39 |
+
|
| 40 |
+
### 📡 API Monitor
|
| 41 |
+
- وضعیت همه ارائهدهندگان
|
| 42 |
+
- زمان پاسخ
|
| 43 |
+
- Health Check
|
| 44 |
+
|
| 45 |
+
### ⚡ Advanced
|
| 46 |
+
- Export JSON/CSV
|
| 47 |
+
- Backup
|
| 48 |
+
- Clear Cache
|
| 49 |
+
- Activity Logs
|
| 50 |
+
|
| 51 |
+
### ⚙️ Admin
|
| 52 |
+
- افزودن API جدید
|
| 53 |
+
- تنظیمات
|
| 54 |
+
- آمار کلی
|
| 55 |
+
|
| 56 |
+
### 🤗 HuggingFace
|
| 57 |
+
- مدلهای Sentiment Analysis
|
| 58 |
+
- Datasets
|
| 59 |
+
- جستجو در Registry
|
| 60 |
+
|
| 61 |
+
### 🔄 Pools
|
| 62 |
+
- مدیریت Poolها
|
| 63 |
+
- افزودن/حذف اعضا
|
| 64 |
+
- چرخش دستی
|
| 65 |
+
|
| 66 |
+
### 📋 Logs (جدید!)
|
| 67 |
+
- نمایش لاگها با فیلتر
|
| 68 |
+
- Export به JSON/CSV
|
| 69 |
+
- جستجو و آمار
|
| 70 |
+
|
| 71 |
+
### 📦 Resources (جدید!)
|
| 72 |
+
- مدیریت منابع API
|
| 73 |
+
- Import/Export
|
| 74 |
+
- Backup
|
| 75 |
+
- فیلتر بر اساس Category
|
| 76 |
+
|
| 77 |
+
## 🔧 استفاده از API
|
| 78 |
+
|
| 79 |
+
### دریافت لاگها
|
| 80 |
```bash
|
| 81 |
+
# همه لاگها
|
| 82 |
+
curl http://localhost:8000/api/logs
|
| 83 |
+
|
| 84 |
+
# فیلتر بر اساس Level
|
| 85 |
+
curl http://localhost:8000/api/logs?level=error
|
| 86 |
+
|
| 87 |
+
# جستجو
|
| 88 |
+
curl http://localhost:8000/api/logs?search=timeout
|
| 89 |
```
|
| 90 |
|
| 91 |
+
### Export لاگها
|
| 92 |
+
```bash
|
| 93 |
+
# Export به JSON
|
| 94 |
+
curl http://localhost:8000/api/logs/export/json?level=error
|
| 95 |
|
| 96 |
+
# Export به CSV
|
| 97 |
+
curl http://localhost:8000/api/logs/export/csv
|
| 98 |
+
```
|
| 99 |
|
| 100 |
+
### مدیریت منابع
|
| 101 |
```bash
|
| 102 |
+
# دریافت همه منابع
|
| 103 |
+
curl http://localhost:8000/api/resources
|
| 104 |
+
|
| 105 |
+
# Export منابع
|
| 106 |
+
curl http://localhost:8000/api/resources/export/json
|
| 107 |
+
|
| 108 |
+
# Backup
|
| 109 |
+
curl -X POST http://localhost:8000/api/resources/backup
|
| 110 |
+
|
| 111 |
+
# Import
|
| 112 |
+
curl -X POST "http://localhost:8000/api/resources/import/json?file_path=api-resources/crypto_resources_unified_2025-11-11.json&merge=true"
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## 📝 مثالهای استفاده
|
| 116 |
+
|
| 117 |
+
### افزودن Provider جدید
|
| 118 |
+
```python
|
| 119 |
+
from resource_manager import ResourceManager
|
| 120 |
+
|
| 121 |
+
manager = ResourceManager()
|
| 122 |
+
|
| 123 |
+
provider = {
|
| 124 |
+
"id": "my_new_api",
|
| 125 |
+
"name": "My New API",
|
| 126 |
+
"category": "market_data",
|
| 127 |
+
"base_url": "https://api.example.com",
|
| 128 |
+
"requires_auth": False,
|
| 129 |
+
"priority": 5,
|
| 130 |
+
"weight": 50,
|
| 131 |
+
"free": True
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
manager.add_provider(provider)
|
| 135 |
+
manager.save_resources()
|
| 136 |
```
|
| 137 |
|
| 138 |
+
### ثبت لاگ
|
| 139 |
+
```python
|
| 140 |
+
from log_manager import log_info, log_error, LogCategory
|
| 141 |
|
| 142 |
+
# لاگ Info
|
| 143 |
+
log_info(LogCategory.PROVIDER, "Provider health check completed",
|
| 144 |
+
provider_id="coingecko", response_time=234.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
+
# لاگ Error
|
| 147 |
+
log_error(LogCategory.PROVIDER, "Provider failed",
|
| 148 |
+
provider_id="etherscan", error="Timeout")
|
| 149 |
+
```
|
| 150 |
|
| 151 |
+
### استفاده از Provider Manager
|
| 152 |
+
```python
|
| 153 |
+
from provider_manager import ProviderManager
|
| 154 |
+
import asyncio
|
| 155 |
+
|
| 156 |
+
async def main():
|
| 157 |
+
manager = ProviderManager()
|
| 158 |
+
|
| 159 |
+
# Health Check
|
| 160 |
+
await manager.health_check_all()
|
| 161 |
+
|
| 162 |
+
# دریافت Provider از Pool
|
| 163 |
+
provider = manager.get_next_from_pool("primary_market_data_pool")
|
| 164 |
+
if provider:
|
| 165 |
+
print(f"Selected: {provider.name}")
|
| 166 |
+
|
| 167 |
+
await manager.close_session()
|
| 168 |
+
|
| 169 |
+
asyncio.run(main())
|
| 170 |
+
```
|
| 171 |
|
| 172 |
+
## 🐳 استفاده با Docker
|
| 173 |
|
|
|
|
| 174 |
```bash
|
| 175 |
+
# Build
|
| 176 |
+
docker build -t crypto-monitor .
|
| 177 |
+
|
| 178 |
+
# Run
|
| 179 |
+
docker run -p 8000:8000 crypto-monitor
|
| 180 |
+
|
| 181 |
+
# یا با docker-compose
|
| 182 |
+
docker-compose up -d
|
| 183 |
```
|
| 184 |
|
| 185 |
+
## 🔍 عیبیابی
|
| 186 |
+
|
| 187 |
+
### مشکل: Port در حال استفاده است
|
| 188 |
```bash
|
| 189 |
+
# تغییر پورت
|
| 190 |
+
uvicorn api_server_extended:app --port 8001
|
| 191 |
```
|
| 192 |
|
| 193 |
+
### مشکل: فایلهای JSON یافت نشد
|
| 194 |
```bash
|
| 195 |
+
# بررسی وجود فایلها
|
| 196 |
+
ls -la api-resources/
|
| 197 |
+
ls -la providers_config*.json
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
### مشکل: Import منابع ناموفق
|
| 201 |
+
```bash
|
| 202 |
+
# بررسی ساختار JSON
|
| 203 |
+
python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
|
| 204 |
```
|
| 205 |
|
| 206 |
+
## 📚 مستندات بیشتر
|
| 207 |
+
|
| 208 |
+
- [README.md](README.md) - مستندات کامل انگلیسی
|
| 209 |
+
- [README_FA.md](README_FA.md) - مستندات کامل فارسی
|
| 210 |
+
- [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
|
| 211 |
+
|
| 212 |
+
## 🆘 پشتیبانی
|
| 213 |
+
|
| 214 |
+
در صورت بروز مشکل:
|
| 215 |
+
1. لاگها را بررسی کنید: `logs/app.log`
|
| 216 |
+
2. از تب Logs در داشبورد استفاده کنید
|
| 217 |
+
3. آمار سیستم را بررسی کنید: `/api/status`
|
| 218 |
|
| 219 |
+
---
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
**موفق باشید! 🚀**
|
TEST_ENDPOINTS.sh
CHANGED
|
@@ -1,161 +1,88 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
#
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
local
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
echo
|
| 25 |
-
|
| 26 |
-
if [ "$
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
echo ""
|
| 47 |
-
|
| 48 |
-
test_endpoint "
|
| 49 |
-
test_endpoint "
|
| 50 |
-
test_endpoint "
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
echo ""
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
test_endpoint "
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
echo ""
|
| 88 |
-
echo "
|
| 89 |
-
|
| 90 |
-
# POST endpoint for sentiment
|
| 91 |
-
echo -n "Testing Sentiment Analysis... "
|
| 92 |
-
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/sentiment/analyze" \
|
| 93 |
-
-H "Content-Type: application/json" \
|
| 94 |
-
-d '{"text":"Bitcoin is breaking new all-time highs!"}')
|
| 95 |
-
http_code=$(echo "$response" | tail -n1)
|
| 96 |
-
body=$(echo "$response" | head -n-1)
|
| 97 |
-
if [ "$http_code" = "200" ]; then
|
| 98 |
-
sentiment=$(echo "$body" | grep -o '"sentiment":"[^"]*"' | cut -d'"' -f4)
|
| 99 |
-
confidence=$(echo "$body" | grep -o '"confidence":[0-9.]*' | cut -d':' -f2)
|
| 100 |
-
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code) - Sentiment: ${YELLOW}$sentiment${NC} (${confidence})"
|
| 101 |
-
else
|
| 102 |
-
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 103 |
-
fi
|
| 104 |
-
|
| 105 |
-
# POST endpoint for query
|
| 106 |
-
echo -n "Testing Query... "
|
| 107 |
-
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/query" \
|
| 108 |
-
-H "Content-Type: application/json" \
|
| 109 |
-
-d '{"query":"What is the price of Bitcoin?"}')
|
| 110 |
-
http_code=$(echo "$response" | tail -n1)
|
| 111 |
-
if [ "$http_code" = "200" ]; then
|
| 112 |
-
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 113 |
-
else
|
| 114 |
-
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 115 |
-
fi
|
| 116 |
-
|
| 117 |
-
# Test provider endpoints
|
| 118 |
-
echo ""
|
| 119 |
-
echo "🔌 Provider Endpoints:"
|
| 120 |
-
test_endpoint "GET" "/api/providers" "" "Providers List"
|
| 121 |
-
|
| 122 |
-
# Test datasets endpoints
|
| 123 |
-
echo ""
|
| 124 |
-
echo "📚 Datasets & Models Endpoints:"
|
| 125 |
-
test_endpoint "GET" "/api/datasets/list" "" "Datasets List"
|
| 126 |
-
test_endpoint "GET" "/api/models/list" "" "Models List"
|
| 127 |
-
|
| 128 |
-
# POST endpoint for model test
|
| 129 |
-
echo -n "Testing Model Test... "
|
| 130 |
-
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/models/test" \
|
| 131 |
-
-H "Content-Type: application/json" \
|
| 132 |
-
-d '{"model":"crypto_sent_0","text":"Ethereum price surging!"}')
|
| 133 |
-
http_code=$(echo "$response" | tail -n1)
|
| 134 |
-
if [ "$http_code" = "200" ]; then
|
| 135 |
-
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 136 |
-
else
|
| 137 |
-
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 138 |
-
fi
|
| 139 |
-
|
| 140 |
-
# Summary
|
| 141 |
-
echo ""
|
| 142 |
-
echo "======================================"
|
| 143 |
-
echo "📊 Test Summary"
|
| 144 |
-
echo "======================================"
|
| 145 |
-
echo ""
|
| 146 |
-
echo "✅ All critical endpoints tested"
|
| 147 |
-
echo ""
|
| 148 |
-
echo "🌐 Dashboard URLs:"
|
| 149 |
-
echo " - Main: $BASE_URL/"
|
| 150 |
-
echo " - Admin: $BASE_URL/admin.html"
|
| 151 |
-
echo " - API Docs: $BASE_URL/docs"
|
| 152 |
-
echo ""
|
| 153 |
-
echo "🔌 WebSocket:"
|
| 154 |
-
echo " - ws://$(echo $BASE_URL | sed 's|http://||')/ws"
|
| 155 |
-
echo ""
|
| 156 |
-
echo "💡 Next steps:"
|
| 157 |
-
echo " 1. Open $BASE_URL/ in your browser"
|
| 158 |
-
echo " 2. Check all dashboard tabs"
|
| 159 |
-
echo " 3. Verify WebSocket connection (status indicator)"
|
| 160 |
-
echo ""
|
| 161 |
-
echo "======================================"
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Script to test all HuggingFace Space endpoints
|
| 3 |
+
|
| 4 |
+
BASE_URL="https://really-amin-datasourceforcryptocurrency.hf.space"
|
| 5 |
+
|
| 6 |
+
echo "=================================="
|
| 7 |
+
echo "🧪 Testing HuggingFace Space API"
|
| 8 |
+
echo "=================================="
|
| 9 |
+
echo ""
|
| 10 |
+
|
| 11 |
+
# Color codes
|
| 12 |
+
GREEN='\033[0;32m'
|
| 13 |
+
RED='\033[0;31m'
|
| 14 |
+
YELLOW='\033[1;33m'
|
| 15 |
+
NC='\033[0m' # No Color
|
| 16 |
+
|
| 17 |
+
test_endpoint() {
|
| 18 |
+
local name=$1
|
| 19 |
+
local endpoint=$2
|
| 20 |
+
|
| 21 |
+
echo -n "Testing $name ... "
|
| 22 |
+
response=$(curl -s -w "\n%{http_code}" "$BASE_URL$endpoint" 2>&1)
|
| 23 |
+
http_code=$(echo "$response" | tail -n1)
|
| 24 |
+
body=$(echo "$response" | head -n-1)
|
| 25 |
+
|
| 26 |
+
if [ "$http_code" = "200" ]; then
|
| 27 |
+
echo -e "${GREEN}✓ OK${NC} (HTTP $http_code)"
|
| 28 |
+
return 0
|
| 29 |
+
else
|
| 30 |
+
echo -e "${RED}✗ FAILED${NC} (HTTP $http_code)"
|
| 31 |
+
echo " Response: $body"
|
| 32 |
+
return 1
|
| 33 |
+
fi
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
# Core Endpoints
|
| 37 |
+
echo "📊 Core Endpoints"
|
| 38 |
+
echo "==================="
|
| 39 |
+
test_endpoint "Health" "/health"
|
| 40 |
+
test_endpoint "Info" "/info"
|
| 41 |
+
test_endpoint "Providers" "/api/providers"
|
| 42 |
+
echo ""
|
| 43 |
+
|
| 44 |
+
# Data Endpoints
|
| 45 |
+
echo "💰 Market Data Endpoints"
|
| 46 |
+
echo "========================="
|
| 47 |
+
test_endpoint "OHLCV (BTC)" "/api/ohlcv?symbol=BTCUSDT&interval=1h&limit=10"
|
| 48 |
+
test_endpoint "Top Prices" "/api/crypto/prices/top?limit=5"
|
| 49 |
+
test_endpoint "BTC Price" "/api/crypto/price/BTC"
|
| 50 |
+
test_endpoint "Market Overview" "/api/crypto/market-overview"
|
| 51 |
+
test_endpoint "Multiple Prices" "/api/market/prices?symbols=BTC,ETH,SOL"
|
| 52 |
+
test_endpoint "Market Data Prices" "/api/market-data/prices?symbols=BTC,ETH"
|
| 53 |
+
echo ""
|
| 54 |
+
|
| 55 |
+
# Analysis Endpoints
|
| 56 |
+
echo "📈 Analysis Endpoints"
|
| 57 |
+
echo "====================="
|
| 58 |
+
test_endpoint "Trading Signals" "/api/analysis/signals?symbol=BTCUSDT"
|
| 59 |
+
test_endpoint "SMC Analysis" "/api/analysis/smc?symbol=BTCUSDT"
|
| 60 |
+
test_endpoint "Scoring Snapshot" "/api/scoring/snapshot?symbol=BTCUSDT"
|
| 61 |
+
test_endpoint "All Signals" "/api/signals"
|
| 62 |
+
test_endpoint "Sentiment" "/api/sentiment"
|
| 63 |
+
echo ""
|
| 64 |
+
|
| 65 |
+
# System Endpoints
|
| 66 |
+
echo "⚙️ System Endpoints"
|
| 67 |
+
echo "===================="
|
| 68 |
+
test_endpoint "System Status" "/api/system/status"
|
| 69 |
+
test_endpoint "System Config" "/api/system/config"
|
| 70 |
+
test_endpoint "Categories" "/api/categories"
|
| 71 |
+
test_endpoint "Rate Limits" "/api/rate-limits"
|
| 72 |
+
test_endpoint "Logs" "/api/logs?limit=10"
|
| 73 |
+
test_endpoint "Alerts" "/api/alerts"
|
| 74 |
+
echo ""
|
| 75 |
+
|
| 76 |
+
# HuggingFace Endpoints
|
| 77 |
+
echo "🤗 HuggingFace Endpoints"
|
| 78 |
+
echo "========================="
|
| 79 |
+
test_endpoint "HF Health" "/api/hf/health"
|
| 80 |
+
test_endpoint "HF Registry" "/api/hf/registry?kind=models"
|
| 81 |
+
echo ""
|
| 82 |
+
|
| 83 |
+
echo "=================================="
|
| 84 |
+
echo "✅ Testing Complete!"
|
| 85 |
+
echo "=================================="
|
| 86 |
+
echo ""
|
| 87 |
+
echo "📖 Full documentation: ${BASE_URL}/docs"
|
| 88 |
+
echo "📋 API Guide: See HUGGINGFACE_API_GUIDE.md"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
admin.html
CHANGED
|
@@ -1,506 +1,1017 @@
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="UTF-8"
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
| 6 |
-
<title>
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
</head>
|
| 13 |
-
<body
|
| 14 |
-
<div class="
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
<
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
</div>
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
</button>
|
| 33 |
-
<button class="
|
| 34 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 17l6-6 4 4 8-8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 35 |
-
Market
|
| 36 |
-
</button>
|
| 37 |
-
<button class="nav-button" data-nav="page-chart">
|
| 38 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/><path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/></svg>
|
| 39 |
-
Chart Lab
|
| 40 |
-
</button>
|
| 41 |
-
<button class="nav-button" data-nav="page-ai">
|
| 42 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" fill="currentColor"/><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 43 |
-
AI Advisor
|
| 44 |
-
</button>
|
| 45 |
-
<button class="nav-button" data-nav="page-news">
|
| 46 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2"/><path d="M7 10h6m-6 4h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 47 |
-
News
|
| 48 |
-
</button>
|
| 49 |
-
<button class="nav-button" data-nav="page-providers">
|
| 50 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
| 51 |
-
Providers
|
| 52 |
-
</button>
|
| 53 |
-
<button class="nav-button" data-nav="page-datasets">
|
| 54 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 12h16M4 17h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 55 |
-
Datasets & Models
|
| 56 |
-
</button>
|
| 57 |
-
<button class="nav-button" data-nav="page-api">
|
| 58 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="currentColor"/></svg>
|
| 59 |
-
API Explorer
|
| 60 |
-
</button>
|
| 61 |
-
<button class="nav-button" data-nav="page-debug">
|
| 62 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 63 |
-
Diagnostics
|
| 64 |
-
</button>
|
| 65 |
-
<button class="nav-button" data-nav="page-settings">
|
| 66 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/><path d="M12 1v6m0 6v6M5 5l4 4m6 6l4 4M1 12h6m6 0h6M5 19l4-4m6-6l4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
| 67 |
-
Settings
|
| 68 |
-
</button>
|
| 69 |
-
</nav>
|
| 70 |
-
<div class="sidebar-footer">
|
| 71 |
-
<small>
|
| 72 |
-
Crypto Intelligence Hub<br />
|
| 73 |
-
<strong>10+ HF Models</strong> • <strong>14 Datasets</strong><br />
|
| 74 |
-
Real-time data • Ensemble sentiment
|
| 75 |
-
</small>
|
| 76 |
</div>
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
<
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
<
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
<
|
| 94 |
-
<
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
</div>
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
<
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
</div>
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
</div>
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
</div>
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
</div>
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
</div>
|
| 243 |
-
|
| 244 |
-
<div class="glass-card">
|
| 245 |
-
<h4>Technical Analysis</h4>
|
| 246 |
-
<div class="controls-bar">
|
| 247 |
-
<label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
|
| 248 |
-
<label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
|
| 249 |
-
<label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
|
| 250 |
-
<label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
|
| 251 |
</div>
|
| 252 |
-
<
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
<!-- ========== AI ADVISOR PAGE ========== -->
|
| 258 |
-
<section id="page-ai" class="page">
|
| 259 |
-
<div class="section-header">
|
| 260 |
-
<h2 class="section-title">AI-Powered Sentiment & Advisory</h2>
|
| 261 |
-
<span class="chip">Ensemble: CryptoBERT + FinBERT + Social</span>
|
| 262 |
-
</div>
|
| 263 |
-
|
| 264 |
-
<div class="glass-card">
|
| 265 |
-
<h4>Sentiment Analysis</h4>
|
| 266 |
-
<form data-sentiment-form>
|
| 267 |
-
<label>Text to Analyze
|
| 268 |
-
<textarea name="text" rows="4" placeholder="Enter crypto-related text, news headline, or social media post for sentiment analysis..."></textarea>
|
| 269 |
-
</label>
|
| 270 |
-
<button class="primary" type="submit">🧠 Analyze Sentiment</button>
|
| 271 |
-
</form>
|
| 272 |
-
<div data-sentiment-result style="margin-top:1rem;"></div>
|
| 273 |
-
</div>
|
| 274 |
-
|
| 275 |
-
<div class="glass-card" style="margin-top:1.5rem;">
|
| 276 |
-
<h4>AI Query Interface</h4>
|
| 277 |
-
<form data-query-form>
|
| 278 |
-
<label>Ask a Question
|
| 279 |
-
<textarea name="query" rows="3" placeholder="e.g., What is the current Bitcoin price? or Analyze Ethereum trend"></textarea>
|
| 280 |
-
</label>
|
| 281 |
-
<button class="primary" type="submit">🔍 Submit Query</button>
|
| 282 |
-
</form>
|
| 283 |
-
<div data-query-result style="margin-top:1rem;"></div>
|
| 284 |
-
</div>
|
| 285 |
-
|
| 286 |
-
<div class="inline-message inline-info">
|
| 287 |
-
⚠️ AI-generated outputs are experimental and should not be considered financial advice.
|
| 288 |
-
</div>
|
| 289 |
-
</section>
|
| 290 |
-
|
| 291 |
-
<!-- ========== NEWS PAGE ========== -->
|
| 292 |
-
<section id="page-news" class="page">
|
| 293 |
-
<div class="section-header">
|
| 294 |
-
<h2 class="section-title">News & Headlines</h2>
|
| 295 |
-
<span class="chip">With AI sentiment analysis</span>
|
| 296 |
-
</div>
|
| 297 |
-
|
| 298 |
-
<div class="controls-bar">
|
| 299 |
-
<input type="text" placeholder="Search headlines..." data-news-search />
|
| 300 |
-
<input type="text" placeholder="Filter by symbol (e.g., BTC)" data-news-symbol />
|
| 301 |
-
<button class="ghost" data-refresh-news>Refresh</button>
|
| 302 |
-
</div>
|
| 303 |
-
|
| 304 |
-
<div class="glass-card">
|
| 305 |
-
<div class="table-wrapper">
|
| 306 |
-
<table>
|
| 307 |
-
<thead>
|
| 308 |
-
<tr>
|
| 309 |
-
<th>Title</th>
|
| 310 |
-
<th>Source</th>
|
| 311 |
-
<th>Symbols</th>
|
| 312 |
-
<th>Sentiment</th>
|
| 313 |
-
<th>Time</th>
|
| 314 |
-
<th>Actions</th>
|
| 315 |
-
</tr>
|
| 316 |
-
</thead>
|
| 317 |
-
<tbody data-news-body>
|
| 318 |
-
<tr><td colspan="6" style="text-align:center;padding:2rem;">Loading news...</td></tr>
|
| 319 |
-
</tbody>
|
| 320 |
-
</table>
|
| 321 |
</div>
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
<section id="page-providers" class="page">
|
| 327 |
-
<div class="section-header">
|
| 328 |
-
<h2 class="section-title">API Providers</h2>
|
| 329 |
-
<span class="chip">95+ data sources</span>
|
| 330 |
-
</div>
|
| 331 |
-
|
| 332 |
-
<div class="glass-card">
|
| 333 |
-
<div class="table-wrapper">
|
| 334 |
-
<table>
|
| 335 |
-
<thead>
|
| 336 |
-
<tr>
|
| 337 |
-
<th>Provider</th>
|
| 338 |
-
<th>Category</th>
|
| 339 |
-
<th>Type</th>
|
| 340 |
-
<th>Status</th>
|
| 341 |
-
<th>Response Time</th>
|
| 342 |
-
</tr>
|
| 343 |
-
</thead>
|
| 344 |
-
<tbody data-providers-body>
|
| 345 |
-
<tr><td colspan="5" style="text-align:center;padding:2rem;">Loading providers...</td></tr>
|
| 346 |
-
</tbody>
|
| 347 |
-
</table>
|
| 348 |
</div>
|
| 349 |
</div>
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
</table>
|
| 394 |
</div>
|
|
|
|
| 395 |
</div>
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
</div>
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
</div>
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
</div>
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
<
|
| 453 |
-
|
| 454 |
-
|
| 455 |
</div>
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
</label>
|
| 492 |
-
</div>
|
| 493 |
-
</div>
|
| 494 |
-
|
| 495 |
-
<div class="inline-message inline-info" style="margin-top:1.5rem;">
|
| 496 |
-
Settings are stored locally in your browser.
|
| 497 |
-
</div>
|
| 498 |
-
</section>
|
| 499 |
-
</div>
|
| 500 |
-
</main>
|
| 501 |
-
</div>
|
| 502 |
-
|
| 503 |
-
<!-- Load App JS as ES6 Module -->
|
| 504 |
-
<script type="module" src="static/js/app.js"></script>
|
| 505 |
</body>
|
| 506 |
</html>
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Admin Dashboard - Crypto Monitor</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
|
| 10 |
+
:root {
|
| 11 |
+
--primary: #667eea;
|
| 12 |
+
--primary-dark: #5568d3;
|
| 13 |
+
--success: #48bb78;
|
| 14 |
+
--warning: #ed8936;
|
| 15 |
+
--danger: #f56565;
|
| 16 |
+
--bg-dark: #1a202c;
|
| 17 |
+
--bg-card: #2d3748;
|
| 18 |
+
--text-light: #e2e8f0;
|
| 19 |
+
--text-muted: #a0aec0;
|
| 20 |
+
--border: #4a5568;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
body {
|
| 24 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 25 |
+
background: var(--bg-dark);
|
| 26 |
+
color: var(--text-light);
|
| 27 |
+
line-height: 1.6;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.container {
|
| 31 |
+
max-width: 1400px;
|
| 32 |
+
margin: 0 auto;
|
| 33 |
+
padding: 20px;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
header {
|
| 37 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
| 38 |
+
padding: 20px;
|
| 39 |
+
border-radius: 10px;
|
| 40 |
+
margin-bottom: 30px;
|
| 41 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
header h1 {
|
| 45 |
+
font-size: 28px;
|
| 46 |
+
font-weight: 700;
|
| 47 |
+
margin-bottom: 5px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
header .subtitle {
|
| 51 |
+
color: rgba(255, 255, 255, 0.9);
|
| 52 |
+
font-size: 14px;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.tabs {
|
| 56 |
+
display: flex;
|
| 57 |
+
gap: 10px;
|
| 58 |
+
margin-bottom: 30px;
|
| 59 |
+
flex-wrap: wrap;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.tab-btn {
|
| 63 |
+
padding: 12px 24px;
|
| 64 |
+
background: var(--bg-card);
|
| 65 |
+
border: 2px solid var(--border);
|
| 66 |
+
border-radius: 8px;
|
| 67 |
+
cursor: pointer;
|
| 68 |
+
font-weight: 600;
|
| 69 |
+
color: var(--text-light);
|
| 70 |
+
transition: all 0.3s;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.tab-btn:hover {
|
| 74 |
+
background: var(--primary);
|
| 75 |
+
border-color: var(--primary);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.tab-btn.active {
|
| 79 |
+
background: var(--primary);
|
| 80 |
+
border-color: var(--primary);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.tab-content {
|
| 84 |
+
display: none;
|
| 85 |
+
animation: fadeIn 0.3s;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.tab-content.active {
|
| 89 |
+
display: block;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@keyframes fadeIn {
|
| 93 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 94 |
+
to { opacity: 1; transform: translateY(0); }
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.card {
|
| 98 |
+
background: var(--bg-card);
|
| 99 |
+
border-radius: 10px;
|
| 100 |
+
padding: 20px;
|
| 101 |
+
margin-bottom: 20px;
|
| 102 |
+
border: 1px solid var(--border);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.card h3 {
|
| 106 |
+
color: var(--primary);
|
| 107 |
+
margin-bottom: 15px;
|
| 108 |
+
font-size: 18px;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.stats-grid {
|
| 112 |
+
display: grid;
|
| 113 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 114 |
+
gap: 15px;
|
| 115 |
+
margin-bottom: 20px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.stat-card {
|
| 119 |
+
background: var(--bg-card);
|
| 120 |
+
padding: 20px;
|
| 121 |
+
border-radius: 8px;
|
| 122 |
+
border: 1px solid var(--border);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.stat-card .label {
|
| 126 |
+
color: var(--text-muted);
|
| 127 |
+
font-size: 12px;
|
| 128 |
+
text-transform: uppercase;
|
| 129 |
+
letter-spacing: 0.5px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.stat-card .value {
|
| 133 |
+
font-size: 32px;
|
| 134 |
+
font-weight: 700;
|
| 135 |
+
color: var(--primary);
|
| 136 |
+
margin: 5px 0;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.stat-card .badge {
|
| 140 |
+
display: inline-block;
|
| 141 |
+
padding: 4px 8px;
|
| 142 |
+
border-radius: 4px;
|
| 143 |
+
font-size: 11px;
|
| 144 |
+
font-weight: 600;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.badge-success {
|
| 148 |
+
background: var(--success);
|
| 149 |
+
color: white;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.badge-warning {
|
| 153 |
+
background: var(--warning);
|
| 154 |
+
color: white;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.badge-danger {
|
| 158 |
+
background: var(--danger);
|
| 159 |
+
color: white;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.btn {
|
| 163 |
+
padding: 10px 20px;
|
| 164 |
+
border: none;
|
| 165 |
+
border-radius: 6px;
|
| 166 |
+
cursor: pointer;
|
| 167 |
+
font-weight: 600;
|
| 168 |
+
transition: all 0.2s;
|
| 169 |
+
margin-right: 10px;
|
| 170 |
+
margin-bottom: 10px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.btn-primary {
|
| 174 |
+
background: var(--primary);
|
| 175 |
+
color: white;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.btn-primary:hover {
|
| 179 |
+
background: var(--primary-dark);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.btn-success {
|
| 183 |
+
background: var(--success);
|
| 184 |
+
color: white;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.btn-success:hover {
|
| 188 |
+
background: #38a169;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.btn-secondary {
|
| 192 |
+
background: var(--bg-card);
|
| 193 |
+
color: var(--text-light);
|
| 194 |
+
border: 1px solid var(--border);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.btn-secondary:hover {
|
| 198 |
+
background: var(--border);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
table {
|
| 202 |
+
width: 100%;
|
| 203 |
+
border-collapse: collapse;
|
| 204 |
+
margin-top: 15px;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
table thead {
|
| 208 |
+
background: var(--bg-dark);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
table th {
|
| 212 |
+
padding: 12px;
|
| 213 |
+
text-align: left;
|
| 214 |
+
font-weight: 600;
|
| 215 |
+
font-size: 12px;
|
| 216 |
+
text-transform: uppercase;
|
| 217 |
+
color: var(--text-muted);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
table td {
|
| 221 |
+
padding: 12px;
|
| 222 |
+
border-top: 1px solid var(--border);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
table tbody tr:hover {
|
| 226 |
+
background: var(--bg-dark);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.status-online {
|
| 230 |
+
color: var(--success);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.status-offline {
|
| 234 |
+
color: var(--danger);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.status-degraded {
|
| 238 |
+
color: var(--warning);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.loading {
|
| 242 |
+
text-align: center;
|
| 243 |
+
padding: 40px;
|
| 244 |
+
color: var(--text-muted);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.error-message {
|
| 248 |
+
background: var(--danger);
|
| 249 |
+
color: white;
|
| 250 |
+
padding: 15px;
|
| 251 |
+
border-radius: 8px;
|
| 252 |
+
margin-bottom: 20px;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.success-message {
|
| 256 |
+
background: var(--success);
|
| 257 |
+
color: white;
|
| 258 |
+
padding: 15px;
|
| 259 |
+
border-radius: 8px;
|
| 260 |
+
margin-bottom: 20px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.empty-state {
|
| 264 |
+
text-align: center;
|
| 265 |
+
padding: 60px 20px;
|
| 266 |
+
color: var(--text-muted);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.empty-state svg {
|
| 270 |
+
width: 64px;
|
| 271 |
+
height: 64px;
|
| 272 |
+
margin-bottom: 20px;
|
| 273 |
+
opacity: 0.3;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.filter-bar {
|
| 277 |
+
display: flex;
|
| 278 |
+
gap: 10px;
|
| 279 |
+
margin-bottom: 20px;
|
| 280 |
+
flex-wrap: wrap;
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
select, input {
|
| 284 |
+
padding: 10px;
|
| 285 |
+
border-radius: 6px;
|
| 286 |
+
border: 1px solid var(--border);
|
| 287 |
+
background: var(--bg-dark);
|
| 288 |
+
color: var(--text-light);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.log-entry {
|
| 292 |
+
padding: 10px;
|
| 293 |
+
border-left: 3px solid var(--primary);
|
| 294 |
+
margin-bottom: 10px;
|
| 295 |
+
background: var(--bg-dark);
|
| 296 |
+
border-radius: 4px;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.log-entry.error {
|
| 300 |
+
border-left-color: var(--danger);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.log-timestamp {
|
| 304 |
+
color: var(--text-muted);
|
| 305 |
+
font-size: 12px;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
pre {
|
| 309 |
+
background: var(--bg-dark);
|
| 310 |
+
padding: 15px;
|
| 311 |
+
border-radius: 6px;
|
| 312 |
+
overflow-x: auto;
|
| 313 |
+
font-size: 13px;
|
| 314 |
+
line-height: 1.4;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.model-card {
|
| 318 |
+
background: var(--bg-dark);
|
| 319 |
+
padding: 15px;
|
| 320 |
+
border-radius: 8px;
|
| 321 |
+
margin-bottom: 15px;
|
| 322 |
+
border-left: 4px solid var(--primary);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.model-card.valid {
|
| 326 |
+
border-left-color: var(--success);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.model-card.conditional {
|
| 330 |
+
border-left-color: var(--warning);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.model-card.invalid {
|
| 334 |
+
border-left-color: var(--danger);
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
@media (max-width: 768px) {
|
| 338 |
+
.stats-grid {
|
| 339 |
+
grid-template-columns: 1fr;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.tabs {
|
| 343 |
+
flex-direction: column;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
table {
|
| 347 |
+
font-size: 12px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
table th, table td {
|
| 351 |
+
padding: 8px;
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
</style>
|
| 355 |
</head>
|
| 356 |
+
<body>
|
| 357 |
+
<div class="container">
|
| 358 |
+
<header>
|
| 359 |
+
<h1>🚀 Crypto Monitor Admin Dashboard</h1>
|
| 360 |
+
<p class="subtitle">Real-time provider management & system monitoring | NO MOCK DATA</p>
|
| 361 |
+
</header>
|
| 362 |
+
|
| 363 |
+
<div class="tabs">
|
| 364 |
+
<button class="tab-btn active" onclick="switchTab('status')">📊 Status</button>
|
| 365 |
+
<button class="tab-btn" onclick="switchTab('providers')">🔌 Providers</button>
|
| 366 |
+
<button class="tab-btn" onclick="switchTab('market')">💰 Market Data</button>
|
| 367 |
+
<button class="tab-btn" onclick="switchTab('apl')">🤖 APL Scanner</button>
|
| 368 |
+
<button class="tab-btn" onclick="switchTab('hf-models')">🧠 HF Models</button>
|
| 369 |
+
<button class="tab-btn" onclick="switchTab('diagnostics')">🔧 Diagnostics</button>
|
| 370 |
+
<button class="tab-btn" onclick="switchTab('logs')">📝 Logs</button>
|
| 371 |
+
</div>
|
| 372 |
+
|
| 373 |
+
<!-- Status Tab -->
|
| 374 |
+
<div id="tab-status" class="tab-content active">
|
| 375 |
+
<div class="stats-grid" id="global-stats">
|
| 376 |
+
<div class="stat-card">
|
| 377 |
+
<div class="label">System Health</div>
|
| 378 |
+
<div class="value" id="system-health">-</div>
|
| 379 |
+
<span class="badge badge-success" id="health-badge">Healthy</span>
|
| 380 |
+
</div>
|
| 381 |
+
<div class="stat-card">
|
| 382 |
+
<div class="label">Total Providers</div>
|
| 383 |
+
<div class="value" id="total-providers">-</div>
|
| 384 |
+
</div>
|
| 385 |
+
<div class="stat-card">
|
| 386 |
+
<div class="label">Validated</div>
|
| 387 |
+
<div class="value" id="validated-providers">-</div>
|
| 388 |
+
</div>
|
| 389 |
+
<div class="stat-card">
|
| 390 |
+
<div class="label">Database</div>
|
| 391 |
+
<div class="value">✓</div>
|
| 392 |
+
<span class="badge badge-success">Connected</span>
|
| 393 |
+
</div>
|
| 394 |
</div>
|
| 395 |
+
|
| 396 |
+
<div class="card">
|
| 397 |
+
<h3>Quick Actions</h3>
|
| 398 |
+
<button class="btn btn-primary" onclick="refreshAllData()">🔄 Refresh All</button>
|
| 399 |
+
<button class="btn btn-success" onclick="runAPL()">🤖 Run APL Scan</button>
|
| 400 |
+
<button class="btn btn-secondary" onclick="runDiagnostics()">🔧 Run Diagnostics</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
</div>
|
| 402 |
+
|
| 403 |
+
<div class="card">
|
| 404 |
+
<h3>Recent Market Data</h3>
|
| 405 |
+
<div id="quick-market-view"></div>
|
| 406 |
+
</div>
|
| 407 |
+
</div>
|
| 408 |
+
|
| 409 |
+
<!-- Providers Tab -->
|
| 410 |
+
<div id="tab-providers" class="tab-content">
|
| 411 |
+
<div class="card">
|
| 412 |
+
<h3>Providers Management</h3>
|
| 413 |
+
<div class="filter-bar">
|
| 414 |
+
<select id="category-filter" onchange="filterProviders()">
|
| 415 |
+
<option value="">All Categories</option>
|
| 416 |
+
<option value="market_data">Market Data</option>
|
| 417 |
+
<option value="sentiment">Sentiment</option>
|
| 418 |
+
<option value="defi">DeFi</option>
|
| 419 |
+
<option value="exchange">Exchange</option>
|
| 420 |
+
<option value="explorer">Explorer</option>
|
| 421 |
+
<option value="rpc">RPC</option>
|
| 422 |
+
<option value="news">News</option>
|
| 423 |
+
</select>
|
| 424 |
+
<button class="btn btn-secondary" onclick="loadProviders()">🔄 Refresh</button>
|
| 425 |
</div>
|
| 426 |
+
<div id="providers-table"></div>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
|
| 430 |
+
<!-- Market Data Tab -->
|
| 431 |
+
<div id="tab-market" class="tab-content">
|
| 432 |
+
<div class="card">
|
| 433 |
+
<h3>Live Market Data</h3>
|
| 434 |
+
<button class="btn btn-primary" onclick="loadMarketData()">🔄 Refresh Prices</button>
|
| 435 |
+
<div id="market-data-container"></div>
|
| 436 |
+
</div>
|
| 437 |
+
|
| 438 |
+
<div class="card">
|
| 439 |
+
<h3>Sentiment Analysis</h3>
|
| 440 |
+
<div id="sentiment-data"></div>
|
| 441 |
+
</div>
|
| 442 |
+
|
| 443 |
+
<div class="card">
|
| 444 |
+
<h3>Trending Coins</h3>
|
| 445 |
+
<div id="trending-coins"></div>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<!-- APL Tab -->
|
| 450 |
+
<div id="tab-apl" class="tab-content">
|
| 451 |
+
<div class="card">
|
| 452 |
+
<h3>Auto Provider Loader (APL)</h3>
|
| 453 |
+
<p style="color: var(--text-muted); margin-bottom: 20px;">
|
| 454 |
+
APL automatically discovers, validates, and integrates cryptocurrency data providers.
|
| 455 |
+
All validations use REAL API calls - NO MOCK DATA.
|
| 456 |
+
</p>
|
| 457 |
+
|
| 458 |
+
<button class="btn btn-success" onclick="runAPL()" id="apl-run-btn">
|
| 459 |
+
🤖 Run APL Scan
|
| 460 |
+
</button>
|
| 461 |
+
<button class="btn btn-secondary" onclick="loadAPLReport()">📊 View Last Report</button>
|
| 462 |
+
|
| 463 |
+
<div id="apl-status" style="margin-top: 20px;"></div>
|
| 464 |
+
</div>
|
| 465 |
+
|
| 466 |
+
<div class="card">
|
| 467 |
+
<h3>APL Summary Statistics</h3>
|
| 468 |
+
<div id="apl-summary"></div>
|
| 469 |
+
</div>
|
| 470 |
+
|
| 471 |
+
<div class="card">
|
| 472 |
+
<h3>APL Output</h3>
|
| 473 |
+
<pre id="apl-output" style="max-height: 400px; overflow-y: auto;">No output yet. Click "Run APL Scan" to start.</pre>
|
| 474 |
+
</div>
|
| 475 |
+
</div>
|
| 476 |
+
|
| 477 |
+
<!-- HF Models Tab -->
|
| 478 |
+
<div id="tab-hf-models" class="tab-content">
|
| 479 |
+
<div class="card">
|
| 480 |
+
<h3>Hugging Face Models</h3>
|
| 481 |
+
<p style="color: var(--text-muted); margin-bottom: 20px;">
|
| 482 |
+
HuggingFace models validated by APL for crypto sentiment analysis and NLP tasks.
|
| 483 |
+
</p>
|
| 484 |
+
<button class="btn btn-primary" onclick="loadHFModels()">🔄 Refresh Models</button>
|
| 485 |
+
<div id="hf-models-container"></div>
|
| 486 |
+
</div>
|
| 487 |
+
|
| 488 |
+
<div class="card">
|
| 489 |
+
<h3>HF Services Health</h3>
|
| 490 |
+
<div id="hf-health"></div>
|
| 491 |
+
</div>
|
| 492 |
+
</div>
|
| 493 |
+
|
| 494 |
+
<!-- Diagnostics Tab -->
|
| 495 |
+
<div id="tab-diagnostics" class="tab-content">
|
| 496 |
+
<div class="card">
|
| 497 |
+
<h3>System Diagnostics</h3>
|
| 498 |
+
<button class="btn btn-primary" onclick="runDiagnostics(true)">🔧 Run with Auto-Fix</button>
|
| 499 |
+
<button class="btn btn-secondary" onclick="runDiagnostics(false)">🔍 Run Scan Only</button>
|
| 500 |
+
<button class="btn btn-secondary" onclick="loadLastDiagnostics()">📋 View Last Results</button>
|
| 501 |
+
|
| 502 |
+
<div id="diagnostics-results" style="margin-top: 20px;"></div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<!-- Logs Tab -->
|
| 507 |
+
<div id="tab-logs" class="tab-content">
|
| 508 |
+
<div class="card">
|
| 509 |
+
<h3>System Logs</h3>
|
| 510 |
+
<button class="btn btn-primary" onclick="loadRecentLogs()">🔄 Refresh</button>
|
| 511 |
+
<button class="btn btn-danger" onclick="loadErrorLogs()">❌ Errors Only</button>
|
| 512 |
+
|
| 513 |
+
<div id="logs-container" style="margin-top: 20px;"></div>
|
| 514 |
+
</div>
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
|
| 518 |
+
<script src="/static/js/api-client.js"></script>
|
| 519 |
+
<script>
|
| 520 |
+
// Tab switching
|
| 521 |
+
function switchTab(tabName) {
|
| 522 |
+
// Hide all tabs
|
| 523 |
+
document.querySelectorAll('.tab-content').forEach(tab => {
|
| 524 |
+
tab.classList.remove('active');
|
| 525 |
+
});
|
| 526 |
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
| 527 |
+
btn.classList.remove('active');
|
| 528 |
+
});
|
| 529 |
+
|
| 530 |
+
// Show selected tab
|
| 531 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 532 |
+
event.target.classList.add('active');
|
| 533 |
+
|
| 534 |
+
// Load data for tab
|
| 535 |
+
switch(tabName) {
|
| 536 |
+
case 'status':
|
| 537 |
+
loadGlobalStatus();
|
| 538 |
+
break;
|
| 539 |
+
case 'providers':
|
| 540 |
+
loadProviders();
|
| 541 |
+
break;
|
| 542 |
+
case 'market':
|
| 543 |
+
loadMarketData();
|
| 544 |
+
loadSentiment();
|
| 545 |
+
loadTrending();
|
| 546 |
+
break;
|
| 547 |
+
case 'apl':
|
| 548 |
+
loadAPLSummary();
|
| 549 |
+
break;
|
| 550 |
+
case 'hf-models':
|
| 551 |
+
loadHFModels();
|
| 552 |
+
loadHFHealth();
|
| 553 |
+
break;
|
| 554 |
+
case 'diagnostics':
|
| 555 |
+
loadLastDiagnostics();
|
| 556 |
+
break;
|
| 557 |
+
case 'logs':
|
| 558 |
+
loadRecentLogs();
|
| 559 |
+
break;
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Global Status
|
| 564 |
+
async function loadGlobalStatus() {
|
| 565 |
+
try {
|
| 566 |
+
const [status, stats] = await Promise.all([
|
| 567 |
+
apiClient.get('/api/status'),
|
| 568 |
+
apiClient.get('/api/stats')
|
| 569 |
+
]);
|
| 570 |
+
|
| 571 |
+
document.getElementById('system-health').textContent = status.system_health.toUpperCase();
|
| 572 |
+
document.getElementById('total-providers').textContent = status.total_providers;
|
| 573 |
+
document.getElementById('validated-providers').textContent = status.validated_providers;
|
| 574 |
+
|
| 575 |
+
// Quick market view
|
| 576 |
+
const market = await apiClient.get('/api/market');
|
| 577 |
+
let marketHTML = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">';
|
| 578 |
+
market.cryptocurrencies.forEach(coin => {
|
| 579 |
+
const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
|
| 580 |
+
marketHTML += `
|
| 581 |
+
<div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
|
| 582 |
+
<div style="font-weight: 600;">${coin.name} (${coin.symbol})</div>
|
| 583 |
+
<div style="font-size: 24px; margin: 10px 0;">$${coin.price.toLocaleString()}</div>
|
| 584 |
+
<div class="${changeClass}">${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%</div>
|
| 585 |
</div>
|
| 586 |
+
`;
|
| 587 |
+
});
|
| 588 |
+
marketHTML += '</div>';
|
| 589 |
+
document.getElementById('quick-market-view').innerHTML = marketHTML;
|
| 590 |
+
|
| 591 |
+
} catch (error) {
|
| 592 |
+
console.error('Error loading global status:', error);
|
| 593 |
+
showError('Failed to load global status');
|
| 594 |
+
}
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
// Providers
|
| 598 |
+
async function loadProviders() {
|
| 599 |
+
try {
|
| 600 |
+
const response = await apiClient.get('/api/providers');
|
| 601 |
+
const providers = response.providers;
|
| 602 |
+
|
| 603 |
+
if (providers.length === 0) {
|
| 604 |
+
document.getElementById('providers-table').innerHTML = '<div class="empty-state">No providers found. Run APL scan to discover providers.</div>';
|
| 605 |
+
return;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
let html = `
|
| 609 |
+
<table>
|
| 610 |
+
<thead>
|
| 611 |
+
<tr>
|
| 612 |
+
<th>Provider ID</th>
|
| 613 |
+
<th>Name</th>
|
| 614 |
+
<th>Category</th>
|
| 615 |
+
<th>Type</th>
|
| 616 |
+
<th>Status</th>
|
| 617 |
+
<th>Response Time</th>
|
| 618 |
+
</tr>
|
| 619 |
+
</thead>
|
| 620 |
+
<tbody>
|
| 621 |
+
`;
|
| 622 |
+
|
| 623 |
+
providers.forEach(p => {
|
| 624 |
+
const statusClass = p.status === 'validated' ? 'status-online' : 'status-degraded';
|
| 625 |
+
html += `
|
| 626 |
+
<tr>
|
| 627 |
+
<td><code>${p.provider_id}</code></td>
|
| 628 |
+
<td>${p.name}</td>
|
| 629 |
+
<td>${p.category}</td>
|
| 630 |
+
<td>${p.type}</td>
|
| 631 |
+
<td class="${statusClass}">${p.status}</td>
|
| 632 |
+
<td>${p.response_time_ms ? p.response_time_ms.toFixed(0) + 'ms' : 'N/A'}</td>
|
| 633 |
+
</tr>
|
| 634 |
+
`;
|
| 635 |
+
});
|
| 636 |
+
|
| 637 |
+
html += '</tbody></table>';
|
| 638 |
+
document.getElementById('providers-table').innerHTML = html;
|
| 639 |
+
|
| 640 |
+
} catch (error) {
|
| 641 |
+
console.error('Error loading providers:', error);
|
| 642 |
+
showError('Failed to load providers');
|
| 643 |
+
}
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
function filterProviders() {
|
| 647 |
+
// Would filter the providers table
|
| 648 |
+
loadProviders();
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
// Market Data
|
| 652 |
+
async function loadMarketData() {
|
| 653 |
+
try {
|
| 654 |
+
const data = await apiClient.get('/api/market');
|
| 655 |
+
|
| 656 |
+
let html = '<table><thead><tr><th>Rank</th><th>Coin</th><th>Price</th><th>24h Change</th><th>Market Cap</th><th>Volume 24h</th></tr></thead><tbody>';
|
| 657 |
+
|
| 658 |
+
data.cryptocurrencies.forEach(coin => {
|
| 659 |
+
const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
|
| 660 |
+
html += `
|
| 661 |
+
<tr>
|
| 662 |
+
<td>${coin.rank}</td>
|
| 663 |
+
<td><strong>${coin.name}</strong> (${coin.symbol})</td>
|
| 664 |
+
<td>$${coin.price.toLocaleString()}</td>
|
| 665 |
+
<td class="${changeClass}">${coin.change_24h >= 0 ? '+' : ''}${coin.change_24h.toFixed(2)}%</td>
|
| 666 |
+
<td>$${(coin.market_cap / 1e9).toFixed(2)}B</td>
|
| 667 |
+
<td>$${(coin.volume_24h / 1e9).toFixed(2)}B</td>
|
| 668 |
+
</tr>
|
| 669 |
+
`;
|
| 670 |
+
});
|
| 671 |
+
|
| 672 |
+
html += '</tbody></table>';
|
| 673 |
+
html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
|
| 674 |
+
|
| 675 |
+
document.getElementById('market-data-container').innerHTML = html;
|
| 676 |
+
} catch (error) {
|
| 677 |
+
console.error('Error loading market data:', error);
|
| 678 |
+
document.getElementById('market-data-container').innerHTML = '<div class="error-message">Failed to load market data: ' + error.message + '</div>';
|
| 679 |
+
}
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
async function loadSentiment() {
|
| 683 |
+
try {
|
| 684 |
+
const data = await apiClient.get('/api/sentiment');
|
| 685 |
+
|
| 686 |
+
const fngValue = data.fear_greed_index;
|
| 687 |
+
let color = '--success';
|
| 688 |
+
if (fngValue < 30) color = '--danger';
|
| 689 |
+
else if (fngValue < 50) color = '--warning';
|
| 690 |
+
|
| 691 |
+
document.getElementById('sentiment-data').innerHTML = `
|
| 692 |
+
<div style="text-align: center; padding: 20px;">
|
| 693 |
+
<div style="font-size: 64px; color: var(${color}); font-weight: 700;">${fngValue}</div>
|
| 694 |
+
<div style="font-size: 24px; margin: 10px 0;">${data.fear_greed_label}</div>
|
| 695 |
+
<p style="color: var(--text-muted);">Source: ${data.source}</p>
|
| 696 |
+
</div>
|
| 697 |
+
`;
|
| 698 |
+
} catch (error) {
|
| 699 |
+
console.error('Error loading sentiment:', error);
|
| 700 |
+
document.getElementById('sentiment-data').innerHTML = '<div class="error-message">Failed to load sentiment: ' + error.message + '</div>';
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
async function loadTrending() {
|
| 705 |
+
try {
|
| 706 |
+
const data = await apiClient.get('/api/trending');
|
| 707 |
+
|
| 708 |
+
if (data.trending.length === 0) {
|
| 709 |
+
document.getElementById('trending-coins').innerHTML = '<div class="empty-state">No trending coins available</div>';
|
| 710 |
+
return;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
let html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">';
|
| 714 |
+
data.trending.forEach(coin => {
|
| 715 |
+
html += `
|
| 716 |
+
<div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
|
| 717 |
+
<div style="font-weight: 600;">${coin.name}</div>
|
| 718 |
+
<div style="color: var(--text-muted);">${coin.symbol}</div>
|
| 719 |
+
${coin.market_cap_rank ? `<div style="margin-top: 10px;">Rank: #${coin.market_cap_rank}</div>` : ''}
|
| 720 |
</div>
|
| 721 |
+
`;
|
| 722 |
+
});
|
| 723 |
+
html += '</div>';
|
| 724 |
+
html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
|
| 725 |
+
|
| 726 |
+
document.getElementById('trending-coins').innerHTML = html;
|
| 727 |
+
} catch (error) {
|
| 728 |
+
console.error('Error loading trending:', error);
|
| 729 |
+
document.getElementById('trending-coins').innerHTML = '<div class="error-message">Failed to load trending: ' + error.message + '</div>';
|
| 730 |
+
}
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
// APL Functions
|
| 734 |
+
async function runAPL() {
|
| 735 |
+
const btn = document.getElementById('apl-run-btn');
|
| 736 |
+
btn.disabled = true;
|
| 737 |
+
btn.textContent = '⏳ Running APL Scan...';
|
| 738 |
+
|
| 739 |
+
document.getElementById('apl-status').innerHTML = '<div class="loading">Running APL scan... This may take 1-2 minutes...</div>';
|
| 740 |
+
document.getElementById('apl-output').textContent = 'Executing APL scan...';
|
| 741 |
+
|
| 742 |
+
try {
|
| 743 |
+
const result = await apiClient.post('/api/apl/run');
|
| 744 |
+
|
| 745 |
+
if (result.status === 'completed') {
|
| 746 |
+
document.getElementById('apl-status').innerHTML = `
|
| 747 |
+
<div class="success-message">
|
| 748 |
+
✓ APL scan completed successfully!<br>
|
| 749 |
+
Providers count: ${result.providers_count}<br>
|
| 750 |
+
Time: ${result.timestamp}
|
| 751 |
</div>
|
| 752 |
+
`;
|
| 753 |
+
document.getElementById('apl-output').textContent = result.stdout || 'Scan completed.';
|
| 754 |
+
|
| 755 |
+
// Reload summary
|
| 756 |
+
await loadAPLSummary();
|
| 757 |
+
|
| 758 |
+
} else {
|
| 759 |
+
document.getElementById('apl-status').innerHTML = `<div class="error-message">APL scan ${result.status}: ${result.message || 'Unknown error'}</div>`;
|
| 760 |
+
document.getElementById('apl-output').textContent = result.stdout || 'No output';
|
| 761 |
+
}
|
| 762 |
+
} catch (error) {
|
| 763 |
+
console.error('Error running APL:', error);
|
| 764 |
+
document.getElementById('apl-status').innerHTML = '<div class="error-message">Failed to run APL: ' + error.message + '</div>';
|
| 765 |
+
} finally {
|
| 766 |
+
btn.disabled = false;
|
| 767 |
+
btn.textContent = '🤖 Run APL Scan';
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
async function loadAPLSummary() {
|
| 772 |
+
try {
|
| 773 |
+
const summary = await apiClient.get('/api/apl/summary');
|
| 774 |
+
|
| 775 |
+
if (summary.status === 'not_available') {
|
| 776 |
+
document.getElementById('apl-summary').innerHTML = '<div class="empty-state">No APL report available. Run APL scan first.</div>';
|
| 777 |
+
return;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
document.getElementById('apl-summary').innerHTML = `
|
| 781 |
+
<div class="stats-grid">
|
| 782 |
+
<div class="stat-card">
|
| 783 |
+
<div class="label">HTTP Candidates</div>
|
| 784 |
+
<div class="value">${summary.http_candidates}</div>
|
| 785 |
+
<span class="badge badge-success">Valid: ${summary.http_valid}</span>
|
| 786 |
</div>
|
| 787 |
+
<div class="stat-card">
|
| 788 |
+
<div class="label">HTTP Invalid</div>
|
| 789 |
+
<div class="value">${summary.http_invalid}</div>
|
| 790 |
+
<span class="badge badge-warning">Conditional: ${summary.http_conditional}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
</div>
|
| 792 |
+
<div class="stat-card">
|
| 793 |
+
<div class="label">HF Models</div>
|
| 794 |
+
<div class="value">${summary.hf_candidates}</div>
|
| 795 |
+
<span class="badge badge-success">Valid: ${summary.hf_valid}</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
</div>
|
| 797 |
+
<div class="stat-card">
|
| 798 |
+
<div class="label">Total Active</div>
|
| 799 |
+
<div class="value">${summary.total_active}</div>
|
| 800 |
+
<span class="badge badge-success">Providers</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
</div>
|
| 802 |
</div>
|
| 803 |
+
<p style="margin-top: 15px; color: var(--text-muted);">Last updated: ${summary.timestamp}</p>
|
| 804 |
+
`;
|
| 805 |
+
} catch (error) {
|
| 806 |
+
console.error('Error loading APL summary:', error);
|
| 807 |
+
document.getElementById('apl-summary').innerHTML = '<div class="error-message">Failed to load APL summary</div>';
|
| 808 |
+
}
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
async function loadAPLReport() {
|
| 812 |
+
try {
|
| 813 |
+
const report = await apiClient.get('/api/apl/report');
|
| 814 |
+
|
| 815 |
+
if (report.status === 'not_available') {
|
| 816 |
+
showError('APL report not available. Run APL scan first.');
|
| 817 |
+
return;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
document.getElementById('apl-output').textContent = JSON.stringify(report, null, 2);
|
| 821 |
+
} catch (error) {
|
| 822 |
+
console.error('Error loading APL report:', error);
|
| 823 |
+
showError('Failed to load APL report');
|
| 824 |
+
}
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
// HF Models
|
| 828 |
+
async function loadHFModels() {
|
| 829 |
+
try {
|
| 830 |
+
const response = await apiClient.get('/api/hf/models');
|
| 831 |
+
|
| 832 |
+
if (response.count === 0) {
|
| 833 |
+
document.getElementById('hf-models-container').innerHTML = '<div class="empty-state">No HF models found. Run APL scan to discover models.</div>';
|
| 834 |
+
return;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
let html = '';
|
| 838 |
+
response.models.forEach(model => {
|
| 839 |
+
const statusClass = model.status === 'VALID' ? 'valid' : model.status === 'CONDITIONALLY_AVAILABLE' ? 'conditional' : 'invalid';
|
| 840 |
+
html += `
|
| 841 |
+
<div class="model-card ${statusClass}">
|
| 842 |
+
<div style="font-weight: 600; margin-bottom: 5px;">${model.provider_name}</div>
|
| 843 |
+
<div style="color: var(--text-muted); font-size: 13px;">${model.provider_id}</div>
|
| 844 |
+
<div style="margin-top: 10px;">
|
| 845 |
+
<span class="badge badge-${statusClass === 'valid' ? 'success' : statusClass === 'conditional' ? 'warning' : 'danger'}">${model.status}</span>
|
|
|
|
| 846 |
</div>
|
| 847 |
+
${model.error_reason ? `<div style="margin-top: 10px; color: var(--warning);">${model.error_reason}</div>` : ''}
|
| 848 |
</div>
|
| 849 |
+
`;
|
| 850 |
+
});
|
| 851 |
+
|
| 852 |
+
document.getElementById('hf-models-container').innerHTML = html;
|
| 853 |
+
} catch (error) {
|
| 854 |
+
console.error('Error loading HF models:', error);
|
| 855 |
+
document.getElementById('hf-models-container').innerHTML = '<div class="error-message">Failed to load HF models</div>';
|
| 856 |
+
}
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
async function loadHFHealth() {
|
| 860 |
+
try {
|
| 861 |
+
const health = await apiClient.get('/api/hf/health');
|
| 862 |
+
|
| 863 |
+
const statusClass = health.ok ? 'status-online' : 'status-offline';
|
| 864 |
+
document.getElementById('hf-health').innerHTML = `
|
| 865 |
+
<div style="padding: 20px; background: var(--bg-dark); border-radius: 8px;">
|
| 866 |
+
<div class="${statusClass}" style="font-size: 24px; font-weight: 700;">${health.ok ? '✓ Healthy' : '✗ Unhealthy'}</div>
|
| 867 |
+
${health.ok ? `
|
| 868 |
+
<div style="margin-top: 15px;">
|
| 869 |
+
<div>Models: ${health.counts.models}</div>
|
| 870 |
+
<div>Datasets: ${health.counts.datasets}</div>
|
| 871 |
+
<div>Last refresh: ${new Date(health.last_refresh_epoch * 1000).toLocaleString()}</div>
|
| 872 |
</div>
|
| 873 |
+
` : `
|
| 874 |
+
<div style="margin-top: 15px; color: var(--danger);">Error: ${health.error || health.fail_reason}</div>
|
| 875 |
+
`}
|
| 876 |
+
</div>
|
| 877 |
+
`;
|
| 878 |
+
} catch (error) {
|
| 879 |
+
console.error('Error loading HF health:', error);
|
| 880 |
+
document.getElementById('hf-health').innerHTML = '<div class="error-message">Failed to load HF health</div>';
|
| 881 |
+
}
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// Diagnostics
|
| 885 |
+
async function runDiagnostics(autoFix = false) {
|
| 886 |
+
try {
|
| 887 |
+
const result = await apiClient.post('/api/diagnostics/run?auto_fix=' + autoFix);
|
| 888 |
+
|
| 889 |
+
let html = `
|
| 890 |
+
<div class="success-message">
|
| 891 |
+
✓ Diagnostics completed<br>
|
| 892 |
+
Issues found: ${result.issues_found}<br>
|
| 893 |
+
Time: ${result.timestamp}
|
| 894 |
+
</div>
|
| 895 |
+
`;
|
| 896 |
+
|
| 897 |
+
if (result.issues.length > 0) {
|
| 898 |
+
html += '<div style="margin-top: 20px;"><h4>Issues Found:</h4>';
|
| 899 |
+
result.issues.forEach(issue => {
|
| 900 |
+
html += `<div class="log-entry error">${issue.type}: ${issue.message}</div>`;
|
| 901 |
+
});
|
| 902 |
+
html += '</div>';
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
if (result.fixes_applied.length > 0) {
|
| 906 |
+
html += '<div style="margin-top: 20px;"><h4>Fixes Applied:</h4>';
|
| 907 |
+
result.fixes_applied.forEach(fix => {
|
| 908 |
+
html += `<div class="log-entry">${fix}</div>`;
|
| 909 |
+
});
|
| 910 |
+
html += '</div>';
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
document.getElementById('diagnostics-results').innerHTML = html;
|
| 914 |
+
} catch (error) {
|
| 915 |
+
console.error('Error running diagnostics:', error);
|
| 916 |
+
showError('Failed to run diagnostics');
|
| 917 |
+
}
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
async function loadLastDiagnostics() {
|
| 921 |
+
try {
|
| 922 |
+
const result = await apiClient.get('/api/diagnostics/last');
|
| 923 |
+
|
| 924 |
+
if (result.status === 'no_previous_run') {
|
| 925 |
+
document.getElementById('diagnostics-results').innerHTML = '<div class="empty-state">No previous diagnostics run found</div>';
|
| 926 |
+
return;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
// Display last diagnostics
|
| 930 |
+
document.getElementById('diagnostics-results').innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
|
| 931 |
+
} catch (error) {
|
| 932 |
+
console.error('Error loading diagnostics:', error);
|
| 933 |
+
showError('Failed to load diagnostics');
|
| 934 |
+
}
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
// Logs
|
| 938 |
+
async function loadRecentLogs() {
|
| 939 |
+
try {
|
| 940 |
+
const response = await apiClient.get('/api/logs/recent');
|
| 941 |
+
|
| 942 |
+
if (response.count === 0) {
|
| 943 |
+
document.getElementById('logs-container').innerHTML = '<div class="empty-state">No logs available</div>';
|
| 944 |
+
return;
|
| 945 |
+
}
|
| 946 |
+
|
| 947 |
+
let html = '';
|
| 948 |
+
response.logs.forEach(log => {
|
| 949 |
+
const className = log.level === 'ERROR' ? 'error' : '';
|
| 950 |
+
html += `
|
| 951 |
+
<div class="log-entry ${className}">
|
| 952 |
+
<div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
|
| 953 |
+
<div>${log.message || JSON.stringify(log)}</div>
|
| 954 |
</div>
|
| 955 |
+
`;
|
| 956 |
+
});
|
| 957 |
+
|
| 958 |
+
document.getElementById('logs-container').innerHTML = html;
|
| 959 |
+
} catch (error) {
|
| 960 |
+
console.error('Error loading logs:', error);
|
| 961 |
+
document.getElementById('logs-container').innerHTML = '<div class="error-message">Failed to load logs</div>';
|
| 962 |
+
}
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
async function loadErrorLogs() {
|
| 966 |
+
try {
|
| 967 |
+
const response = await apiClient.get('/api/logs/errors');
|
| 968 |
+
|
| 969 |
+
if (response.count === 0) {
|
| 970 |
+
document.getElementById('logs-container').innerHTML = '<div class="empty-state">No error logs found</div>';
|
| 971 |
+
return;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
let html = '';
|
| 975 |
+
response.errors.forEach(log => {
|
| 976 |
+
html += `
|
| 977 |
+
<div class="log-entry error">
|
| 978 |
+
<div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
|
| 979 |
+
<div>${log.message || JSON.stringify(log)}</div>
|
| 980 |
</div>
|
| 981 |
+
`;
|
| 982 |
+
});
|
| 983 |
+
|
| 984 |
+
document.getElementById('logs-container').innerHTML = html;
|
| 985 |
+
} catch (error) {
|
| 986 |
+
console.error('Error loading error logs:', error);
|
| 987 |
+
showError('Failed to load error logs');
|
| 988 |
+
}
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
// Utility functions
|
| 992 |
+
function showError(message) {
|
| 993 |
+
alert('Error: ' + message);
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
function refreshAllData() {
|
| 997 |
+
loadGlobalStatus();
|
| 998 |
+
loadProviders();
|
| 999 |
+
loadAPLSummary();
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
// Auto-refresh every 30 seconds
|
| 1003 |
+
setInterval(() => {
|
| 1004 |
+
const activeTab = document.querySelector('.tab-content.active').id;
|
| 1005 |
+
if (activeTab === 'tab-status') {
|
| 1006 |
+
loadGlobalStatus();
|
| 1007 |
+
}
|
| 1008 |
+
}, 30000);
|
| 1009 |
+
|
| 1010 |
+
// Load initial data
|
| 1011 |
+
window.addEventListener('DOMContentLoaded', () => {
|
| 1012 |
+
console.log('✓ Admin Dashboard Loaded - Real Data Only');
|
| 1013 |
+
loadGlobalStatus();
|
| 1014 |
+
});
|
| 1015 |
+
</script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
</body>
|
| 1017 |
</html>
|
config.js
CHANGED
|
@@ -1,366 +1,146 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
-
*
|
| 4 |
-
* Dashboard Settings - Easy Customization
|
| 5 |
-
* ═══════════════════════════════════════════════════════════════════
|
| 6 |
*/
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
//
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
//
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
MAX_RECONNECT_ATTEMPTS: 5,
|
| 25 |
-
RECONNECT_DELAY: 3000, // 3 seconds
|
| 26 |
-
|
| 27 |
-
// ═══════════════════════════════════════════════════════════════
|
| 28 |
-
// Display Settings
|
| 29 |
-
// ═══════════════════════════════════════════════════════════════
|
| 30 |
-
|
| 31 |
-
// Number of items to display
|
| 32 |
-
MAX_COINS_DISPLAY: 20, // Number of coins in table
|
| 33 |
-
MAX_NEWS_DISPLAY: 20, // Number of news items
|
| 34 |
-
MAX_TRENDING_DISPLAY: 10, // Number of trending items
|
| 35 |
-
|
| 36 |
-
// Table settings
|
| 37 |
-
TABLE_ROWS_PER_PAGE: 10,
|
| 38 |
-
|
| 39 |
-
// ═══════════════════════════════════════════════════════════════
|
| 40 |
-
// Chart Settings
|
| 41 |
-
// ═══════════════════════════════════════════════════════════════
|
| 42 |
-
|
| 43 |
-
CHART: {
|
| 44 |
-
DEFAULT_SYMBOL: 'BTCUSDT',
|
| 45 |
-
DEFAULT_INTERVAL: '1h',
|
| 46 |
-
AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
|
| 47 |
-
THEME: 'dark',
|
| 48 |
-
},
|
| 49 |
-
|
| 50 |
-
// ═══════════════════════════════════════════════════════════════
|
| 51 |
-
// AI Settings
|
| 52 |
-
// ═══════════════════════════════════════════════════════════════
|
| 53 |
-
|
| 54 |
-
AI: {
|
| 55 |
-
ENABLE_SENTIMENT: true,
|
| 56 |
-
ENABLE_NEWS_SUMMARY: true,
|
| 57 |
-
ENABLE_PRICE_PREDICTION: false, // Currently disabled
|
| 58 |
-
ENABLE_PATTERN_DETECTION: false, // Currently disabled
|
| 59 |
-
},
|
| 60 |
-
|
| 61 |
-
// ═══════════════════════════════════════════════════════════════
|
| 62 |
-
// Notification Settings
|
| 63 |
-
// ═══════════════════════════════════════════════════════════════
|
| 64 |
-
|
| 65 |
-
NOTIFICATIONS: {
|
| 66 |
-
ENABLE: true,
|
| 67 |
-
SHOW_PRICE_ALERTS: true,
|
| 68 |
-
SHOW_NEWS_ALERTS: true,
|
| 69 |
-
AUTO_DISMISS_TIME: 5000, // 5 seconds
|
| 70 |
-
},
|
| 71 |
-
|
| 72 |
-
// ═══════════════════════════════════════════════════════════════
|
| 73 |
-
// UI Settings
|
| 74 |
-
// ═══════════════════════════════════════════════════════════════
|
| 75 |
-
|
| 76 |
-
UI: {
|
| 77 |
-
DEFAULT_THEME: 'dark', // 'dark' or 'light'
|
| 78 |
-
ENABLE_ANIMATIONS: true,
|
| 79 |
-
ENABLE_SOUNDS: false,
|
| 80 |
-
LANGUAGE: 'en', // 'en' or 'fa'
|
| 81 |
-
RTL: false,
|
| 82 |
-
},
|
| 83 |
-
|
| 84 |
-
// ═══════════════════════════════════════════════════════════════
|
| 85 |
-
// Debug Settings
|
| 86 |
-
// ═══════════════════════════════════════════════════════════════
|
| 87 |
-
|
| 88 |
-
DEBUG: {
|
| 89 |
-
ENABLE_CONSOLE_LOGS: true,
|
| 90 |
-
ENABLE_PERFORMANCE_MONITORING: true,
|
| 91 |
-
SHOW_API_REQUESTS: true,
|
| 92 |
-
SHOW_WS_MESSAGES: false,
|
| 93 |
-
},
|
| 94 |
-
|
| 95 |
-
// ═══════════════════════════════════════════════════════════════
|
| 96 |
-
// Default Filters and Sorting
|
| 97 |
-
// ═══════════════════════════════════════════════════════════════
|
| 98 |
-
|
| 99 |
-
FILTERS: {
|
| 100 |
-
DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
|
| 101 |
-
DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
|
| 102 |
-
DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
|
| 103 |
-
SORT_ORDER: 'desc', // 'asc' or 'desc'
|
| 104 |
-
},
|
| 105 |
-
|
| 106 |
-
// ═══════════════════════════════════════════════════════════════
|
| 107 |
-
// API Endpoints (Optional - if your backend differs)
|
| 108 |
-
// ═══════════════════════════════════════════════════════════════
|
| 109 |
-
|
| 110 |
-
ENDPOINTS: {
|
| 111 |
-
HEALTH: '/api/health',
|
| 112 |
-
MARKET: '/api/market/stats',
|
| 113 |
-
MARKET_PRICES: '/api/market/prices',
|
| 114 |
-
COINS_TOP: '/api/coins/top',
|
| 115 |
-
COIN_DETAILS: '/api/coins',
|
| 116 |
-
TRENDING: '/api/trending',
|
| 117 |
-
SENTIMENT: '/api/sentiment',
|
| 118 |
-
SENTIMENT_ANALYZE: '/api/sentiment/analyze',
|
| 119 |
-
NEWS: '/api/news/latest',
|
| 120 |
-
NEWS_SUMMARIZE: '/api/news/summarize',
|
| 121 |
-
STATS: '/api/stats',
|
| 122 |
-
PROVIDERS: '/api/providers',
|
| 123 |
-
PROVIDER_STATUS: '/api/providers/status',
|
| 124 |
-
CHART_HISTORY: '/api/charts/price',
|
| 125 |
-
CHART_ANALYZE: '/api/charts/analyze',
|
| 126 |
-
OHLCV: '/api/ohlcv',
|
| 127 |
-
QUERY: '/api/query',
|
| 128 |
-
DATASETS: '/api/datasets/list',
|
| 129 |
-
MODELS: '/api/models/list',
|
| 130 |
-
HF_HEALTH: '/api/hf/health',
|
| 131 |
-
HF_REGISTRY: '/api/hf/registry',
|
| 132 |
-
SYSTEM_STATUS: '/api/system/status',
|
| 133 |
-
SYSTEM_CONFIG: '/api/system/config',
|
| 134 |
-
CATEGORIES: '/api/categories',
|
| 135 |
-
RATE_LIMITS: '/api/rate-limits',
|
| 136 |
-
LOGS: '/api/logs',
|
| 137 |
-
ALERTS: '/api/alerts',
|
| 138 |
-
},
|
| 139 |
-
|
| 140 |
-
// ═══════════════════════════════════════════════════════════════
|
| 141 |
-
// WebSocket Events
|
| 142 |
-
// ═══════════════════════════════════════════════════════════════
|
| 143 |
-
|
| 144 |
-
WS_EVENTS: {
|
| 145 |
-
MARKET_UPDATE: 'market_update',
|
| 146 |
-
SENTIMENT_UPDATE: 'sentiment_update',
|
| 147 |
-
NEWS_UPDATE: 'news_update',
|
| 148 |
-
STATS_UPDATE: 'stats_update',
|
| 149 |
-
PRICE_UPDATE: 'price_update',
|
| 150 |
-
API_UPDATE: 'api_update',
|
| 151 |
-
STATUS_UPDATE: 'status_update',
|
| 152 |
-
SCHEDULE_UPDATE: 'schedule_update',
|
| 153 |
-
CONNECTED: 'connected',
|
| 154 |
-
DISCONNECTED: 'disconnected',
|
| 155 |
-
},
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
|
|
|
| 160 |
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
},
|
| 177 |
-
},
|
| 178 |
-
|
| 179 |
-
// ═══════════════════════════════════════════════════════════════
|
| 180 |
-
// Rate Limiting
|
| 181 |
-
// ═══════════════════════════════════════════════════════════════
|
| 182 |
-
|
| 183 |
-
RATE_LIMITS: {
|
| 184 |
-
API_REQUESTS_PER_MINUTE: 60,
|
| 185 |
-
SEARCH_DEBOUNCE_MS: 300,
|
| 186 |
-
},
|
| 187 |
-
|
| 188 |
-
// ═══════════════════════════════════════════════════════════════
|
| 189 |
-
// Storage Settings
|
| 190 |
-
// ═══════════════════════════════════════════════════════════════
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
// High Performance Profile
|
| 206 |
-
HIGH_PERFORMANCE: {
|
| 207 |
-
UPDATE_INTERVAL: 15000, // Faster updates
|
| 208 |
-
CACHE_TTL: 30000, // Shorter cache
|
| 209 |
-
ENABLE_ANIMATIONS: false, // No animations
|
| 210 |
-
MAX_COINS_DISPLAY: 50,
|
| 211 |
-
},
|
| 212 |
-
|
| 213 |
-
// Data Saver Profile
|
| 214 |
-
DATA_SAVER: {
|
| 215 |
-
UPDATE_INTERVAL: 60000, // Less frequent updates
|
| 216 |
-
CACHE_TTL: 300000, // Longer cache (5 minutes)
|
| 217 |
-
MAX_COINS_DISPLAY: 10,
|
| 218 |
-
MAX_NEWS_DISPLAY: 10,
|
| 219 |
-
},
|
| 220 |
-
|
| 221 |
-
// Presentation Profile
|
| 222 |
-
PRESENTATION: {
|
| 223 |
-
ENABLE_ANIMATIONS: true,
|
| 224 |
-
UPDATE_INTERVAL: 20000,
|
| 225 |
-
SHOW_API_REQUESTS: false,
|
| 226 |
-
ENABLE_CONSOLE_LOGS: false,
|
| 227 |
-
},
|
| 228 |
-
|
| 229 |
-
// Development Profile
|
| 230 |
-
DEVELOPMENT: {
|
| 231 |
-
DEBUG: {
|
| 232 |
-
ENABLE_CONSOLE_LOGS: true,
|
| 233 |
-
ENABLE_PERFORMANCE_MONITORING: true,
|
| 234 |
-
SHOW_API_REQUESTS: true,
|
| 235 |
-
SHOW_WS_MESSAGES: true,
|
| 236 |
},
|
| 237 |
-
UPDATE_INTERVAL: 10000,
|
| 238 |
-
},
|
| 239 |
-
};
|
| 240 |
-
|
| 241 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 242 |
-
// Helper Function to Change Profile
|
| 243 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 244 |
-
|
| 245 |
-
window.applyDashboardProfile = function (profileName) {
|
| 246 |
-
if (window.DASHBOARD_PROFILES[profileName]) {
|
| 247 |
-
const profile = window.DASHBOARD_PROFILES[profileName];
|
| 248 |
-
Object.assign(window.DASHBOARD_CONFIG, profile);
|
| 249 |
-
console.log(`✅ Profile "${profileName}" applied`);
|
| 250 |
-
|
| 251 |
-
// Reload application with new settings
|
| 252 |
-
if (window.app) {
|
| 253 |
-
window.app.destroy();
|
| 254 |
-
window.app = new DashboardApp();
|
| 255 |
-
window.app.init();
|
| 256 |
-
}
|
| 257 |
-
} else {
|
| 258 |
-
console.error(`❌ Profile "${profileName}" not found`);
|
| 259 |
-
}
|
| 260 |
-
};
|
| 261 |
-
|
| 262 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 263 |
-
// Helper Function to Change Backend URL
|
| 264 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 265 |
-
|
| 266 |
-
window.changeBackendURL = function (httpUrl, wsUrl) {
|
| 267 |
-
window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
|
| 268 |
-
window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
|
| 269 |
-
|
| 270 |
-
console.log('✅ Backend URL changed:');
|
| 271 |
-
console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
|
| 272 |
-
console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
|
| 273 |
-
|
| 274 |
-
// Reload application
|
| 275 |
-
if (window.app) {
|
| 276 |
-
window.app.destroy();
|
| 277 |
-
window.app = new DashboardApp();
|
| 278 |
-
window.app.init();
|
| 279 |
-
}
|
| 280 |
-
};
|
| 281 |
-
|
| 282 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 283 |
-
// Save Settings to LocalStorage
|
| 284 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 285 |
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
localStorage.setItem(
|
| 291 |
-
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
|
| 292 |
-
configString
|
| 293 |
-
);
|
| 294 |
-
console.log('✅ Settings saved');
|
| 295 |
-
} catch (error) {
|
| 296 |
-
console.error('❌ Error saving settings:', error);
|
| 297 |
-
}
|
| 298 |
-
}
|
| 299 |
-
};
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
console.
|
|
|
|
| 315 |
}
|
| 316 |
-
}
|
| 317 |
-
console.error('❌ Error loading settings:', error);
|
| 318 |
-
}
|
| 319 |
-
}
|
| 320 |
-
};
|
| 321 |
|
| 322 |
-
//
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
});
|
| 330 |
-
} else {
|
| 331 |
-
window.loadConfig();
|
| 332 |
}
|
| 333 |
|
| 334 |
-
//
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
╚═══════════════════════════════════════════════════════════════╝
|
| 342 |
-
|
| 343 |
-
📋 Available Commands:
|
| 344 |
-
|
| 345 |
-
1. Change Profile:
|
| 346 |
-
applyDashboardProfile('HIGH_PERFORMANCE')
|
| 347 |
-
applyDashboardProfile('DATA_SAVER')
|
| 348 |
-
applyDashboardProfile('PRESENTATION')
|
| 349 |
-
applyDashboardProfile('DEVELOPMENT')
|
| 350 |
-
|
| 351 |
-
2. Change Backend:
|
| 352 |
-
changeBackendURL('https://your-backend.com')
|
| 353 |
-
|
| 354 |
-
3. Save/Load Settings:
|
| 355 |
-
saveConfig()
|
| 356 |
-
loadConfig()
|
| 357 |
-
|
| 358 |
-
4. View Current Settings:
|
| 359 |
-
console.log(DASHBOARD_CONFIG)
|
| 360 |
-
|
| 361 |
-
5. Manual Settings Change:
|
| 362 |
-
DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
|
| 363 |
-
saveConfig()
|
| 364 |
-
|
| 365 |
-
═══════════════════════════════════════════════════════════════════
|
| 366 |
-
`);
|
|
|
|
| 1 |
/**
|
| 2 |
+
* API Configuration for Crypto API Monitoring System
|
| 3 |
+
* Automatically detects environment (localhost, HuggingFace Spaces, or custom deployment)
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
+
const CONFIG = (() => {
|
| 7 |
+
// Detect if running on HuggingFace Spaces
|
| 8 |
+
const isHuggingFaceSpaces = window.location.hostname.includes('hf.space') ||
|
| 9 |
+
window.location.hostname.includes('huggingface.co');
|
| 10 |
+
|
| 11 |
+
// Detect if running locally
|
| 12 |
+
const isLocalhost = window.location.hostname === 'localhost' ||
|
| 13 |
+
window.location.hostname === '127.0.0.1' ||
|
| 14 |
+
window.location.hostname === '';
|
| 15 |
+
|
| 16 |
+
// Get base API URL based on environment
|
| 17 |
+
const getApiBaseUrl = () => {
|
| 18 |
+
// If running on HuggingFace Spaces, use relative URLs
|
| 19 |
+
if (isHuggingFaceSpaces) {
|
| 20 |
+
return window.location.origin;
|
| 21 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
// If running locally, use localhost with port 7860
|
| 24 |
+
if (isLocalhost) {
|
| 25 |
+
return 'http://localhost:7860';
|
| 26 |
+
}
|
| 27 |
|
| 28 |
+
// For custom deployments, use the current origin
|
| 29 |
+
return window.location.origin;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// Get WebSocket URL based on environment
|
| 33 |
+
const getWebSocketUrl = () => {
|
| 34 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 35 |
+
const host = isLocalhost ? 'localhost:7860' : window.location.host;
|
| 36 |
+
return `${protocol}//${host}`;
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const API_BASE = getApiBaseUrl();
|
| 40 |
+
const WS_BASE = getWebSocketUrl();
|
| 41 |
+
|
| 42 |
+
return {
|
| 43 |
+
// API Configuration
|
| 44 |
+
API_BASE: API_BASE,
|
| 45 |
+
WS_BASE: WS_BASE,
|
| 46 |
+
|
| 47 |
+
// Environment flags
|
| 48 |
+
IS_HUGGINGFACE_SPACES: isHuggingFaceSpaces,
|
| 49 |
+
IS_LOCALHOST: isLocalhost,
|
| 50 |
+
|
| 51 |
+
// API Endpoints
|
| 52 |
+
ENDPOINTS: {
|
| 53 |
+
// Health & Status
|
| 54 |
+
HEALTH: `${API_BASE}/health`,
|
| 55 |
+
API_INFO: `${API_BASE}/api-info`,
|
| 56 |
+
STATUS: `${API_BASE}/api/status`,
|
| 57 |
+
|
| 58 |
+
// Provider Management
|
| 59 |
+
PROVIDERS: `${API_BASE}/api/providers`,
|
| 60 |
+
CATEGORIES: `${API_BASE}/api/categories`,
|
| 61 |
+
|
| 62 |
+
// Data Collection
|
| 63 |
+
PRICES: `${API_BASE}/api/prices`,
|
| 64 |
+
NEWS: `${API_BASE}/api/news`,
|
| 65 |
+
SENTIMENT: `${API_BASE}/api/sentiment/current`,
|
| 66 |
+
WHALES: `${API_BASE}/api/whales/transactions`,
|
| 67 |
+
|
| 68 |
+
// HuggingFace Integration
|
| 69 |
+
HF_HEALTH: `${API_BASE}/api/hf/health`,
|
| 70 |
+
HF_REGISTRY: `${API_BASE}/api/hf/registry`,
|
| 71 |
+
HF_SEARCH: `${API_BASE}/api/hf/search`,
|
| 72 |
+
HF_REFRESH: `${API_BASE}/api/hf/refresh`,
|
| 73 |
+
HF_RUN_SENTIMENT: `${API_BASE}/api/hf/run-sentiment`,
|
| 74 |
+
|
| 75 |
+
// Monitoring
|
| 76 |
+
LOGS: `${API_BASE}/api/logs`,
|
| 77 |
+
ALERTS: `${API_BASE}/api/alerts`,
|
| 78 |
+
SCHEDULER: `${API_BASE}/api/scheduler/status`,
|
| 79 |
+
|
| 80 |
+
// Analytics
|
| 81 |
+
ANALYTICS: `${API_BASE}/api/analytics/failures`,
|
| 82 |
+
RATE_LIMITS: `${API_BASE}/api/rate-limits`,
|
| 83 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
// WebSocket Endpoints
|
| 86 |
+
WEBSOCKETS: {
|
| 87 |
+
MASTER: `${WS_BASE}/ws`,
|
| 88 |
+
LIVE: `${WS_BASE}/ws/live`,
|
| 89 |
+
DATA: `${WS_BASE}/ws/data`,
|
| 90 |
+
MARKET_DATA: `${WS_BASE}/ws/market_data`,
|
| 91 |
+
NEWS: `${WS_BASE}/ws/news`,
|
| 92 |
+
SENTIMENT: `${WS_BASE}/ws/sentiment`,
|
| 93 |
+
WHALE_TRACKING: `${WS_BASE}/ws/whale_tracking`,
|
| 94 |
+
HEALTH: `${WS_BASE}/ws/health`,
|
| 95 |
+
MONITORING: `${WS_BASE}/ws/monitoring`,
|
| 96 |
+
HUGGINGFACE: `${WS_BASE}/ws/huggingface`,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
// Utility Functions
|
| 100 |
+
buildUrl: (path) => {
|
| 101 |
+
return `${API_BASE}${path}`;
|
| 102 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
buildWsUrl: (path) => {
|
| 105 |
+
return `${WS_BASE}${path}`;
|
| 106 |
+
},
|
| 107 |
|
| 108 |
+
// Fetch helper with error handling
|
| 109 |
+
fetchJSON: async (url, options = {}) => {
|
| 110 |
+
try {
|
| 111 |
+
const response = await fetch(url, options);
|
| 112 |
+
if (!response.ok) {
|
| 113 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 114 |
+
}
|
| 115 |
+
return await response.json();
|
| 116 |
+
} catch (error) {
|
| 117 |
+
console.error(`Fetch error for ${url}:`, error);
|
| 118 |
+
throw error;
|
| 119 |
}
|
| 120 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
// POST helper
|
| 123 |
+
postJSON: async (url, body = {}) => {
|
| 124 |
+
return CONFIG.fetchJSON(url, {
|
| 125 |
+
method: 'POST',
|
| 126 |
+
headers: {
|
| 127 |
+
'Content-Type': 'application/json',
|
| 128 |
+
},
|
| 129 |
+
body: JSON.stringify(body),
|
| 130 |
+
});
|
| 131 |
+
},
|
| 132 |
+
};
|
| 133 |
+
})();
|
| 134 |
|
| 135 |
+
// Export for use in modules (if needed)
|
| 136 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 137 |
+
module.exports = CONFIG;
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
+
// Log configuration on load (for debugging)
|
| 141 |
+
console.log('🚀 Crypto API Monitor - Configuration loaded:', {
|
| 142 |
+
environment: CONFIG.IS_HUGGINGFACE_SPACES ? 'HuggingFace Spaces' :
|
| 143 |
+
CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
|
| 144 |
+
apiBase: CONFIG.API_BASE,
|
| 145 |
+
wsBase: CONFIG.WS_BASE,
|
| 146 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hf_unified_server.py
CHANGED
|
@@ -432,45 +432,8 @@ async def get_market_overview():
|
|
| 432 |
raise HTTPException(status_code=503, detail="Unable to fetch market data")
|
| 433 |
|
| 434 |
# Calculate market stats
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
total_volume = 0
|
| 438 |
-
|
| 439 |
-
for p in prices:
|
| 440 |
-
# Try different field names for market cap
|
| 441 |
-
market_cap = (
|
| 442 |
-
p.get("market_cap") or
|
| 443 |
-
p.get("market_cap_usd") or
|
| 444 |
-
p.get("market_cap_rank") or # Sometimes this is the value
|
| 445 |
-
None
|
| 446 |
-
)
|
| 447 |
-
# If market_cap is not found, try calculating from price and supply
|
| 448 |
-
if not market_cap:
|
| 449 |
-
price = p.get("price") or p.get("current_price") or 0
|
| 450 |
-
supply = p.get("circulating_supply") or p.get("total_supply") or 0
|
| 451 |
-
if price and supply:
|
| 452 |
-
market_cap = float(price) * float(supply)
|
| 453 |
-
|
| 454 |
-
if market_cap:
|
| 455 |
-
try:
|
| 456 |
-
total_market_cap += float(market_cap)
|
| 457 |
-
except (TypeError, ValueError):
|
| 458 |
-
pass
|
| 459 |
-
|
| 460 |
-
# Try different field names for volume
|
| 461 |
-
volume = (
|
| 462 |
-
p.get("total_volume") or
|
| 463 |
-
p.get("volume_24h") or
|
| 464 |
-
p.get("volume_24h_usd") or
|
| 465 |
-
None
|
| 466 |
-
)
|
| 467 |
-
if volume:
|
| 468 |
-
try:
|
| 469 |
-
total_volume += float(volume)
|
| 470 |
-
except (TypeError, ValueError):
|
| 471 |
-
pass
|
| 472 |
-
|
| 473 |
-
logger.info(f"Market overview: {len(prices)} coins, total_market_cap={total_market_cap:,.0f}, total_volume={total_volume:,.0f}")
|
| 474 |
|
| 475 |
# Sort by 24h change
|
| 476 |
gainers = sorted(
|
|
@@ -909,14 +872,6 @@ async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
|
|
| 909 |
# HTML Routes - Serve UI files
|
| 910 |
# ============================================================================
|
| 911 |
|
| 912 |
-
@app.get("/favicon.ico")
|
| 913 |
-
async def favicon():
|
| 914 |
-
"""Serve favicon"""
|
| 915 |
-
favicon_path = WORKSPACE_ROOT / "static" / "favicon.ico"
|
| 916 |
-
if favicon_path.exists():
|
| 917 |
-
return FileResponse(favicon_path)
|
| 918 |
-
return JSONResponse({"status": "no favicon"}, status_code=404)
|
| 919 |
-
|
| 920 |
@app.get("/", response_class=HTMLResponse)
|
| 921 |
async def root():
|
| 922 |
"""Serve main admin dashboard (admin.html)"""
|
|
@@ -989,187 +944,6 @@ async def serve_html(filename: str):
|
|
| 989 |
# Startup Event
|
| 990 |
# ============================================================================
|
| 991 |
|
| 992 |
-
|
| 993 |
-
# ============================================================================
|
| 994 |
-
# ADMIN DASHBOARD ENDPOINTS
|
| 995 |
-
# ============================================================================
|
| 996 |
-
|
| 997 |
-
from fastapi import WebSocket, WebSocketDisconnect
|
| 998 |
-
import asyncio
|
| 999 |
-
|
| 1000 |
-
class ConnectionManager:
|
| 1001 |
-
def __init__(self):
|
| 1002 |
-
self.active_connections = []
|
| 1003 |
-
async def connect(self, websocket: WebSocket):
|
| 1004 |
-
await websocket.accept()
|
| 1005 |
-
self.active_connections.append(websocket)
|
| 1006 |
-
def disconnect(self, websocket: WebSocket):
|
| 1007 |
-
if websocket in self.active_connections:
|
| 1008 |
-
self.active_connections.remove(websocket)
|
| 1009 |
-
async def broadcast(self, message: dict):
|
| 1010 |
-
for conn in list(self.active_connections):
|
| 1011 |
-
try:
|
| 1012 |
-
await conn.send_json(message)
|
| 1013 |
-
except:
|
| 1014 |
-
self.disconnect(conn)
|
| 1015 |
-
|
| 1016 |
-
ws_manager = ConnectionManager()
|
| 1017 |
-
|
| 1018 |
-
@app.get("/api/health")
|
| 1019 |
-
async def api_health():
|
| 1020 |
-
h = await health()
|
| 1021 |
-
return {"status": "healthy" if h.get("status") == "ok" else "degraded", **h}
|
| 1022 |
-
|
| 1023 |
-
@app.get("/api/coins/top")
|
| 1024 |
-
async def get_top_coins(limit: int = Query(default=10, ge=1, le=100)):
|
| 1025 |
-
coins = await market_collector.get_top_coins(limit=limit)
|
| 1026 |
-
result = [{"rank": c.get("rank", 0), "symbol": c.get("symbol", "").upper(), "name": c.get("name", ""),
|
| 1027 |
-
"price": c.get("price") or c.get("current_price", 0),
|
| 1028 |
-
"price_change_24h": c.get("change_24h") or c.get("price_change_percentage_24h", 0),
|
| 1029 |
-
"volume_24h": c.get("volume_24h") or c.get("total_volume", 0), "market_cap": c.get("market_cap", 0)} for c in coins]
|
| 1030 |
-
return {"success": True, "coins": result, "count": len(result)}
|
| 1031 |
-
|
| 1032 |
-
@app.get("/api/coins/{symbol}")
|
| 1033 |
-
async def get_coin_detail(symbol: str):
|
| 1034 |
-
coins = await market_collector.get_top_coins(limit=250)
|
| 1035 |
-
coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
|
| 1036 |
-
if not coin:
|
| 1037 |
-
raise HTTPException(404, f"Coin {symbol} not found")
|
| 1038 |
-
return {"success": True, "symbol": symbol.upper(), "name": coin.get("name", ""),
|
| 1039 |
-
"price": coin.get("price") or coin.get("current_price", 0),
|
| 1040 |
-
"change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
|
| 1041 |
-
"market_cap": coin.get("market_cap", 0)}
|
| 1042 |
-
|
| 1043 |
-
@app.get("/api/market/stats")
|
| 1044 |
-
async def get_market_stats():
|
| 1045 |
-
"""Get global market statistics (duplicate endpoint - keeping for compatibility)"""
|
| 1046 |
-
try:
|
| 1047 |
-
overview = await get_market_overview()
|
| 1048 |
-
|
| 1049 |
-
# Calculate ETH dominance from prices if available
|
| 1050 |
-
eth_dominance = 0
|
| 1051 |
-
if overview.get("total_market_cap", 0) > 0:
|
| 1052 |
-
try:
|
| 1053 |
-
eth_prices = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
|
| 1054 |
-
if eth_prices and len(eth_prices) > 0:
|
| 1055 |
-
eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
|
| 1056 |
-
eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
|
| 1057 |
-
except:
|
| 1058 |
-
pass
|
| 1059 |
-
|
| 1060 |
-
return {
|
| 1061 |
-
"success": True,
|
| 1062 |
-
"stats": {
|
| 1063 |
-
"total_market_cap": overview.get("total_market_cap", 0) or 0,
|
| 1064 |
-
"total_volume_24h": overview.get("total_volume_24h", 0) or 0,
|
| 1065 |
-
"btc_dominance": overview.get("btc_dominance", 0) or 0,
|
| 1066 |
-
"eth_dominance": eth_dominance,
|
| 1067 |
-
"active_cryptocurrencies": 10000,
|
| 1068 |
-
"markets": 500,
|
| 1069 |
-
"market_cap_change_24h": 0.0,
|
| 1070 |
-
"timestamp": datetime.now().isoformat()
|
| 1071 |
-
}
|
| 1072 |
-
}
|
| 1073 |
-
except Exception as e:
|
| 1074 |
-
logger.error(f"Error in /api/market/stats (duplicate): {e}")
|
| 1075 |
-
return {
|
| 1076 |
-
"success": True,
|
| 1077 |
-
"stats": {
|
| 1078 |
-
"total_market_cap": 0,
|
| 1079 |
-
"total_volume_24h": 0,
|
| 1080 |
-
"btc_dominance": 0,
|
| 1081 |
-
"eth_dominance": 0,
|
| 1082 |
-
"active_cryptocurrencies": 0,
|
| 1083 |
-
"markets": 0,
|
| 1084 |
-
"market_cap_change_24h": 0.0,
|
| 1085 |
-
"timestamp": datetime.now().isoformat()
|
| 1086 |
-
}
|
| 1087 |
-
}
|
| 1088 |
-
|
| 1089 |
-
@app.get("/api/news/latest")
|
| 1090 |
-
async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
|
| 1091 |
-
from ai_models import analyze_news_item
|
| 1092 |
-
news = await news_collector.get_latest_news(limit=limit)
|
| 1093 |
-
enriched = []
|
| 1094 |
-
for item in news[:limit]:
|
| 1095 |
-
try:
|
| 1096 |
-
e = analyze_news_item(item)
|
| 1097 |
-
enriched.append({"title": e.get("title", ""), "source": e.get("source", ""),
|
| 1098 |
-
"published_at": e.get("published_at") or e.get("date", ""),
|
| 1099 |
-
"symbols": e.get("symbols", []), "sentiment": e.get("sentiment", "neutral"),
|
| 1100 |
-
"sentiment_confidence": e.get("sentiment_confidence", 0.5)})
|
| 1101 |
-
except:
|
| 1102 |
-
enriched.append({"title": item.get("title", ""), "source": item.get("source", ""),
|
| 1103 |
-
"published_at": item.get("date", ""), "symbols": item.get("symbols", []),
|
| 1104 |
-
"sentiment": "neutral", "sentiment_confidence": 0.5})
|
| 1105 |
-
return {"success": True, "news": enriched, "count": len(enriched)}
|
| 1106 |
-
|
| 1107 |
-
@app.post("/api/news/summarize")
|
| 1108 |
-
async def summarize_news(item: Dict[str, Any] = Body(...)):
|
| 1109 |
-
from ai_models import analyze_news_item
|
| 1110 |
-
e = analyze_news_item(item)
|
| 1111 |
-
return {"success": True, "summary": e.get("title", ""), "sentiment": e.get("sentiment", "neutral")}
|
| 1112 |
-
|
| 1113 |
-
# Duplicate endpoints removed - using the improved versions below in CHARTS ENDPOINTS section
|
| 1114 |
-
|
| 1115 |
-
@app.post("/api/sentiment/analyze")
|
| 1116 |
-
async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
|
| 1117 |
-
from ai_models import ensemble_crypto_sentiment
|
| 1118 |
-
result = ensemble_crypto_sentiment(payload.get("text", ""))
|
| 1119 |
-
return {"success": True, "sentiment": result["label"], "confidence": result["confidence"], "details": result}
|
| 1120 |
-
|
| 1121 |
-
@app.post("/api/query")
|
| 1122 |
-
async def process_query(payload: Dict[str, Any] = Body(...)):
|
| 1123 |
-
query = payload.get("query", "").lower()
|
| 1124 |
-
if "price" in query or "btc" in query:
|
| 1125 |
-
coins = await market_collector.get_top_coins(limit=10)
|
| 1126 |
-
btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
|
| 1127 |
-
if btc:
|
| 1128 |
-
return {"success": True, "type": "price", "message": f"Bitcoin is ${btc.get('price', 0):,.2f}", "data": btc}
|
| 1129 |
-
return {"success": True, "type": "general", "message": "Query processed"}
|
| 1130 |
-
|
| 1131 |
-
@app.get("/api/datasets/list")
|
| 1132 |
-
async def list_datasets():
|
| 1133 |
-
from backend.services.hf_registry import REGISTRY
|
| 1134 |
-
datasets = REGISTRY.list(kind="datasets")
|
| 1135 |
-
formatted = [{"name": d.get("id"), "category": d.get("category", "other"), "tags": d.get("tags", [])} for d in datasets]
|
| 1136 |
-
return {"success": True, "datasets": formatted, "count": len(formatted)}
|
| 1137 |
-
|
| 1138 |
-
@app.get("/api/datasets/sample")
|
| 1139 |
-
async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
|
| 1140 |
-
return {"success": False, "name": name, "sample": [], "message": "Auth required"}
|
| 1141 |
-
|
| 1142 |
-
@app.get("/api/models/list")
|
| 1143 |
-
async def list_models():
|
| 1144 |
-
from ai_models import get_model_info
|
| 1145 |
-
info = get_model_info()
|
| 1146 |
-
models = []
|
| 1147 |
-
for cat, mlist in info.get("model_catalog", {}).items():
|
| 1148 |
-
for mid in mlist:
|
| 1149 |
-
models.append({"name": mid, "task": "sentiment" if "sentiment" in cat else "analysis", "category": cat})
|
| 1150 |
-
return {"success": True, "models": models, "count": len(models)}
|
| 1151 |
-
|
| 1152 |
-
@app.post("/api/models/test")
|
| 1153 |
-
async def test_model(payload: Dict[str, Any] = Body(...)):
|
| 1154 |
-
from ai_models import ensemble_crypto_sentiment
|
| 1155 |
-
result = ensemble_crypto_sentiment(payload.get("text", ""))
|
| 1156 |
-
return {"success": True, "model": payload.get("model", ""), "result": result}
|
| 1157 |
-
|
| 1158 |
-
@app.websocket("/ws")
|
| 1159 |
-
async def websocket_endpoint(websocket: WebSocket):
|
| 1160 |
-
await ws_manager.connect(websocket)
|
| 1161 |
-
try:
|
| 1162 |
-
while True:
|
| 1163 |
-
top_coins = await market_collector.get_top_coins(limit=5)
|
| 1164 |
-
news = await news_collector.get_latest_news(limit=3)
|
| 1165 |
-
from ai_models import ensemble_crypto_sentiment
|
| 1166 |
-
sentiment = ensemble_crypto_sentiment(" ".join([n.get("title", "") for n in news])) if news else {"label": "neutral", "confidence": 0.5}
|
| 1167 |
-
await websocket.send_json({"type": "update", "payload": {"market_data": top_coins, "news": news, "sentiment": sentiment, "timestamp": datetime.now().isoformat()}})
|
| 1168 |
-
await asyncio.sleep(10)
|
| 1169 |
-
except WebSocketDisconnect:
|
| 1170 |
-
ws_manager.disconnect(websocket)
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
@app.on_event("startup")
|
| 1174 |
async def startup_event():
|
| 1175 |
"""Initialize on startup"""
|
|
@@ -1337,26 +1111,14 @@ async def get_coin_detail(symbol: str):
|
|
| 1337 |
async def get_market_stats():
|
| 1338 |
"""Get global market statistics"""
|
| 1339 |
try:
|
| 1340 |
-
# Use existing endpoint
|
| 1341 |
overview = await get_market_overview()
|
| 1342 |
|
| 1343 |
-
# Calculate ETH dominance from prices if available
|
| 1344 |
-
eth_dominance = 0
|
| 1345 |
-
if overview.get("total_market_cap", 0) > 0:
|
| 1346 |
-
# Try to get ETH market cap from top coins
|
| 1347 |
-
try:
|
| 1348 |
-
eth_prices = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
|
| 1349 |
-
if eth_prices and len(eth_prices) > 0:
|
| 1350 |
-
eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
|
| 1351 |
-
eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
|
| 1352 |
-
except:
|
| 1353 |
-
pass
|
| 1354 |
-
|
| 1355 |
stats = {
|
| 1356 |
-
"total_market_cap": overview.get("
|
| 1357 |
-
"total_volume_24h": overview.get("
|
| 1358 |
-
"btc_dominance": overview.get("btc_dominance", 0)
|
| 1359 |
-
"eth_dominance": eth_dominance,
|
| 1360 |
"active_cryptocurrencies": 10000, # Approximate
|
| 1361 |
"markets": 500, # Approximate
|
| 1362 |
"market_cap_change_24h": 0.0,
|
|
@@ -1441,112 +1203,30 @@ async def summarize_news(item: Dict[str, Any] = Body(...)):
|
|
| 1441 |
async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
|
| 1442 |
"""Get price chart data"""
|
| 1443 |
try:
|
| 1444 |
-
#
|
| 1445 |
-
|
| 1446 |
-
|
| 1447 |
-
return JSONResponse(
|
| 1448 |
-
status_code=400,
|
| 1449 |
-
content={
|
| 1450 |
-
"success": False,
|
| 1451 |
-
"symbol": "",
|
| 1452 |
-
"timeframe": timeframe,
|
| 1453 |
-
"data": [],
|
| 1454 |
-
"count": 0,
|
| 1455 |
-
"error": "Symbol cannot be empty"
|
| 1456 |
-
}
|
| 1457 |
-
)
|
| 1458 |
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
# market_collector.get_price_history expects timeframe as string, not hours
|
| 1462 |
-
price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
|
| 1463 |
-
|
| 1464 |
-
if not price_history or len(price_history) == 0:
|
| 1465 |
-
logger.warning(f"No price history returned for {symbol}")
|
| 1466 |
-
return {
|
| 1467 |
-
"success": True,
|
| 1468 |
-
"symbol": symbol,
|
| 1469 |
-
"timeframe": timeframe,
|
| 1470 |
-
"data": [],
|
| 1471 |
-
"count": 0,
|
| 1472 |
-
"message": "No data available"
|
| 1473 |
-
}
|
| 1474 |
|
| 1475 |
chart_data = []
|
| 1476 |
for point in price_history:
|
| 1477 |
-
# Handle different timestamp formats
|
| 1478 |
-
timestamp = point.get("timestamp") or point.get("time") or point.get("date")
|
| 1479 |
-
price = point.get("price") or point.get("close") or point.get("value") or 0
|
| 1480 |
-
|
| 1481 |
-
# Convert timestamp to ISO format if needed
|
| 1482 |
-
if timestamp:
|
| 1483 |
-
try:
|
| 1484 |
-
# If it's already a string, use it
|
| 1485 |
-
if isinstance(timestamp, str):
|
| 1486 |
-
# Try to parse and format
|
| 1487 |
-
try:
|
| 1488 |
-
# Try ISO format first
|
| 1489 |
-
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
| 1490 |
-
timestamp = dt.isoformat()
|
| 1491 |
-
except:
|
| 1492 |
-
try:
|
| 1493 |
-
# Try other common formats
|
| 1494 |
-
from dateutil import parser
|
| 1495 |
-
dt = parser.parse(timestamp)
|
| 1496 |
-
timestamp = dt.isoformat()
|
| 1497 |
-
except:
|
| 1498 |
-
pass
|
| 1499 |
-
elif isinstance(timestamp, (int, float)):
|
| 1500 |
-
# Unix timestamp
|
| 1501 |
-
dt = datetime.fromtimestamp(timestamp)
|
| 1502 |
-
timestamp = dt.isoformat()
|
| 1503 |
-
except Exception as e:
|
| 1504 |
-
logger.warning(f"Error parsing timestamp {timestamp}: {e}")
|
| 1505 |
-
|
| 1506 |
chart_data.append({
|
| 1507 |
-
"timestamp": timestamp
|
| 1508 |
-
"
|
| 1509 |
-
"date": timestamp
|
| 1510 |
-
"price": float(price) if price else 0,
|
| 1511 |
-
"close": float(price) if price else 0,
|
| 1512 |
-
"value": float(price) if price else 0
|
| 1513 |
})
|
| 1514 |
|
| 1515 |
-
logger.info(f"Returning {len(chart_data)} data points for {symbol}")
|
| 1516 |
-
|
| 1517 |
return {
|
| 1518 |
"success": True,
|
| 1519 |
-
"symbol": symbol,
|
| 1520 |
"timeframe": timeframe,
|
| 1521 |
"data": chart_data,
|
| 1522 |
"count": len(chart_data)
|
| 1523 |
}
|
| 1524 |
-
except CollectorError as e:
|
| 1525 |
-
logger.error(f"Collector error in /api/charts/price/{symbol}: {e}", exc_info=True)
|
| 1526 |
-
return JSONResponse(
|
| 1527 |
-
status_code=200,
|
| 1528 |
-
content={
|
| 1529 |
-
"success": False,
|
| 1530 |
-
"symbol": symbol.upper() if symbol else "",
|
| 1531 |
-
"timeframe": timeframe,
|
| 1532 |
-
"data": [],
|
| 1533 |
-
"count": 0,
|
| 1534 |
-
"error": str(e)
|
| 1535 |
-
}
|
| 1536 |
-
)
|
| 1537 |
except Exception as e:
|
| 1538 |
-
logger.error(f"Error in /api/charts/price/{symbol}: {e}"
|
| 1539 |
-
|
| 1540 |
-
status_code=200,
|
| 1541 |
-
content={
|
| 1542 |
-
"success": False,
|
| 1543 |
-
"symbol": symbol.upper() if symbol else "",
|
| 1544 |
-
"timeframe": timeframe,
|
| 1545 |
-
"data": [],
|
| 1546 |
-
"count": 0,
|
| 1547 |
-
"error": str(e)
|
| 1548 |
-
}
|
| 1549 |
-
)
|
| 1550 |
|
| 1551 |
|
| 1552 |
@app.post("/api/charts/analyze")
|
|
@@ -1557,38 +1237,12 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
|
|
| 1557 |
timeframe = payload.get("timeframe", "7d")
|
| 1558 |
indicators = payload.get("indicators", [])
|
| 1559 |
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
status_code=400,
|
| 1563 |
-
content={"success": False, "error": "Symbol is required"}
|
| 1564 |
-
)
|
| 1565 |
-
|
| 1566 |
-
symbol = symbol.strip().upper()
|
| 1567 |
-
logger.info(f"Analyzing chart for {symbol} with timeframe {timeframe}")
|
| 1568 |
-
|
| 1569 |
-
# Get price data - use timeframe string, not hours
|
| 1570 |
-
price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
|
| 1571 |
-
|
| 1572 |
-
if not price_history or len(price_history) == 0:
|
| 1573 |
-
return {
|
| 1574 |
-
"success": False,
|
| 1575 |
-
"symbol": symbol,
|
| 1576 |
-
"timeframe": timeframe,
|
| 1577 |
-
"error": "No price data available for analysis"
|
| 1578 |
-
}
|
| 1579 |
|
| 1580 |
# Analyze with AI
|
| 1581 |
from ai_models import analyze_chart_points
|
| 1582 |
-
|
| 1583 |
-
analysis = analyze_chart_points(price_history, indicators)
|
| 1584 |
-
except Exception as ai_error:
|
| 1585 |
-
logger.error(f"AI analysis error: {ai_error}", exc_info=True)
|
| 1586 |
-
# Return a basic analysis if AI fails
|
| 1587 |
-
analysis = {
|
| 1588 |
-
"direction": "neutral",
|
| 1589 |
-
"summary": "Analysis unavailable",
|
| 1590 |
-
"signals": []
|
| 1591 |
-
}
|
| 1592 |
|
| 1593 |
return {
|
| 1594 |
"success": True,
|
|
@@ -1596,18 +1250,9 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
|
|
| 1596 |
"timeframe": timeframe,
|
| 1597 |
"analysis": analysis
|
| 1598 |
}
|
| 1599 |
-
except CollectorError as e:
|
| 1600 |
-
logger.error(f"Collector error in /api/charts/analyze: {e}", exc_info=True)
|
| 1601 |
-
return JSONResponse(
|
| 1602 |
-
status_code=200,
|
| 1603 |
-
content={"success": False, "error": str(e)}
|
| 1604 |
-
)
|
| 1605 |
except Exception as e:
|
| 1606 |
-
logger.error(f"Error in /api/charts/analyze: {e}"
|
| 1607 |
-
return
|
| 1608 |
-
status_code=200,
|
| 1609 |
-
content={"success": False, "error": str(e)}
|
| 1610 |
-
)
|
| 1611 |
|
| 1612 |
|
| 1613 |
# ===== SENTIMENT ENDPOINTS =====
|
|
|
|
| 432 |
raise HTTPException(status_code=503, detail="Unable to fetch market data")
|
| 433 |
|
| 434 |
# Calculate market stats
|
| 435 |
+
total_market_cap = sum(p.get("market_cap", 0) or 0 for p in prices)
|
| 436 |
+
total_volume = sum(p.get("total_volume", 0) or 0 for p in prices)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
# Sort by 24h change
|
| 439 |
gainers = sorted(
|
|
|
|
| 872 |
# HTML Routes - Serve UI files
|
| 873 |
# ============================================================================
|
| 874 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
@app.get("/", response_class=HTMLResponse)
|
| 876 |
async def root():
|
| 877 |
"""Serve main admin dashboard (admin.html)"""
|
|
|
|
| 944 |
# Startup Event
|
| 945 |
# ============================================================================
|
| 946 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
@app.on_event("startup")
|
| 948 |
async def startup_event():
|
| 949 |
"""Initialize on startup"""
|
|
|
|
| 1111 |
async def get_market_stats():
|
| 1112 |
"""Get global market statistics"""
|
| 1113 |
try:
|
| 1114 |
+
# Use existing endpoint
|
| 1115 |
overview = await get_market_overview()
|
| 1116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
stats = {
|
| 1118 |
+
"total_market_cap": overview.get("global_market_cap", 0),
|
| 1119 |
+
"total_volume_24h": overview.get("global_volume", 0),
|
| 1120 |
+
"btc_dominance": overview.get("btc_dominance", 0),
|
| 1121 |
+
"eth_dominance": overview.get("eth_dominance", 0),
|
| 1122 |
"active_cryptocurrencies": 10000, # Approximate
|
| 1123 |
"markets": 500, # Approximate
|
| 1124 |
"market_cap_change_24h": 0.0,
|
|
|
|
| 1203 |
async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
|
| 1204 |
"""Get price chart data"""
|
| 1205 |
try:
|
| 1206 |
+
# Map timeframe to hours
|
| 1207 |
+
timeframe_map = {"1d": 24, "7d": 168, "30d": 720, "90d": 2160, "1y": 8760}
|
| 1208 |
+
hours = timeframe_map.get(timeframe, 168)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
|
| 1210 |
+
price_history = await market_collector.get_price_history(symbol, hours=hours)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
|
| 1212 |
chart_data = []
|
| 1213 |
for point in price_history:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
chart_data.append({
|
| 1215 |
+
"timestamp": point.get("timestamp", ""),
|
| 1216 |
+
"price": point.get("price", 0),
|
| 1217 |
+
"date": point.get("timestamp", "")
|
|
|
|
|
|
|
|
|
|
| 1218 |
})
|
| 1219 |
|
|
|
|
|
|
|
| 1220 |
return {
|
| 1221 |
"success": True,
|
| 1222 |
+
"symbol": symbol.upper(),
|
| 1223 |
"timeframe": timeframe,
|
| 1224 |
"data": chart_data,
|
| 1225 |
"count": len(chart_data)
|
| 1226 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1227 |
except Exception as e:
|
| 1228 |
+
logger.error(f"Error in /api/charts/price/{symbol}: {e}")
|
| 1229 |
+
raise HTTPException(status_code=503, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
|
| 1231 |
|
| 1232 |
@app.post("/api/charts/analyze")
|
|
|
|
| 1237 |
timeframe = payload.get("timeframe", "7d")
|
| 1238 |
indicators = payload.get("indicators", [])
|
| 1239 |
|
| 1240 |
+
# Get price data
|
| 1241 |
+
price_history = await market_collector.get_price_history(symbol, hours=168)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
|
| 1243 |
# Analyze with AI
|
| 1244 |
from ai_models import analyze_chart_points
|
| 1245 |
+
analysis = analyze_chart_points(price_history, indicators)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1246 |
|
| 1247 |
return {
|
| 1248 |
"success": True,
|
|
|
|
| 1250 |
"timeframe": timeframe,
|
| 1251 |
"analysis": analysis
|
| 1252 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
except Exception as e:
|
| 1254 |
+
logger.error(f"Error in /api/charts/analyze: {e}")
|
| 1255 |
+
return {"success": False, "error": str(e)}
|
|
|
|
|
|
|
|
|
|
| 1256 |
|
| 1257 |
|
| 1258 |
# ===== SENTIMENT ENDPOINTS =====
|
index.html
CHANGED
|
@@ -1,765 +1,1216 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en"
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
<
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
<div class="
|
| 422 |
-
<div class="
|
| 423 |
-
|
| 424 |
-
<
|
| 425 |
-
<
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
</
|
| 473 |
-
</div>
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
</div>
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
<div class="
|
| 509 |
-
<h3>
|
| 510 |
-
<
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
<
|
| 518 |
-
<div class="
|
| 519 |
-
<
|
| 520 |
-
<div class="
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
<
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
<div
|
| 597 |
-
<
|
| 598 |
-
<
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
</div>
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
<
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 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>Crypto API Monitor</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
* { box-sizing: border-box; }
|
| 11 |
+
:root {
|
| 12 |
+
--bg: #f5f6fb;
|
| 13 |
+
--card: #ffffff;
|
| 14 |
+
--muted: #6b7280;
|
| 15 |
+
--text: #1f2937;
|
| 16 |
+
--primary: #2563eb;
|
| 17 |
+
--success: #16a34a;
|
| 18 |
+
--danger: #dc2626;
|
| 19 |
+
--warning: #d97706;
|
| 20 |
+
--border: #e5e7eb;
|
| 21 |
+
}
|
| 22 |
+
body {
|
| 23 |
+
margin: 0;
|
| 24 |
+
font-family: 'Inter', sans-serif;
|
| 25 |
+
background: var(--bg);
|
| 26 |
+
color: var(--text);
|
| 27 |
+
}
|
| 28 |
+
.container {
|
| 29 |
+
max-width: 1400px;
|
| 30 |
+
margin: 0 auto;
|
| 31 |
+
padding: 20px;
|
| 32 |
+
}
|
| 33 |
+
.header {
|
| 34 |
+
background: var(--card);
|
| 35 |
+
padding: 18px 24px;
|
| 36 |
+
border-radius: 10px;
|
| 37 |
+
box-shadow: 0 1px 3px rgba(15,23,42,0.08);
|
| 38 |
+
display: flex;
|
| 39 |
+
justify-content: space-between;
|
| 40 |
+
align-items: center;
|
| 41 |
+
flex-wrap: wrap;
|
| 42 |
+
gap: 16px;
|
| 43 |
+
}
|
| 44 |
+
.logo h1 { margin: 0; font-size: 20px; color: var(--primary); }
|
| 45 |
+
.header-actions { display: flex; gap: 12px; align-items: center; }
|
| 46 |
+
.status-pill {
|
| 47 |
+
padding: 6px 12px;
|
| 48 |
+
border-radius: 999px;
|
| 49 |
+
font-size: 12px;
|
| 50 |
+
font-weight: 600;
|
| 51 |
+
background: #fee2e2;
|
| 52 |
+
color: var(--danger);
|
| 53 |
+
}
|
| 54 |
+
.btn {
|
| 55 |
+
padding: 10px 18px;
|
| 56 |
+
border-radius: 8px;
|
| 57 |
+
border: none;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
font-size: 13px;
|
| 60 |
+
cursor: pointer;
|
| 61 |
+
background: var(--primary);
|
| 62 |
+
color: #fff;
|
| 63 |
+
transition: all 0.2s ease;
|
| 64 |
+
display: inline-flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 6px;
|
| 67 |
+
box-shadow: 0 2px 4px rgba(37,99,235,0.2);
|
| 68 |
+
}
|
| 69 |
+
.btn:hover {
|
| 70 |
+
background: #1d4ed8;
|
| 71 |
+
transform: translateY(-1px);
|
| 72 |
+
box-shadow: 0 4px 8px rgba(37,99,235,0.3);
|
| 73 |
+
}
|
| 74 |
+
.btn:active {
|
| 75 |
+
transform: translateY(0);
|
| 76 |
+
box-shadow: 0 1px 2px rgba(37,99,235,0.2);
|
| 77 |
+
}
|
| 78 |
+
.btn.secondary {
|
| 79 |
+
background: var(--card);
|
| 80 |
+
border: 1px solid var(--border);
|
| 81 |
+
color: var(--text);
|
| 82 |
+
box-shadow: 0 1px 2px rgba(15,23,42,0.1);
|
| 83 |
+
}
|
| 84 |
+
.btn.secondary:hover {
|
| 85 |
+
background: #f9fafb;
|
| 86 |
+
border-color: var(--primary);
|
| 87 |
+
color: var(--primary);
|
| 88 |
+
box-shadow: 0 2px 4px rgba(37,99,235,0.15);
|
| 89 |
+
}
|
| 90 |
+
.kpi-grid {
|
| 91 |
+
display: grid;
|
| 92 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 93 |
+
gap: 10px;
|
| 94 |
+
margin: 16px 0;
|
| 95 |
+
}
|
| 96 |
+
.kpi-card {
|
| 97 |
+
background: var(--card);
|
| 98 |
+
padding: 14px;
|
| 99 |
+
border-radius: 8px;
|
| 100 |
+
box-shadow: 0 1px 3px rgba(15,23,42,0.08);
|
| 101 |
+
border: 1px solid var(--border);
|
| 102 |
+
transition: all 0.2s ease;
|
| 103 |
+
display: flex;
|
| 104 |
+
flex-direction: column;
|
| 105 |
+
gap: 8px;
|
| 106 |
+
}
|
| 107 |
+
.kpi-card:hover {
|
| 108 |
+
box-shadow: 0 4px 12px rgba(37,99,235,0.15);
|
| 109 |
+
transform: translateY(-2px);
|
| 110 |
+
border-color: var(--primary);
|
| 111 |
+
}
|
| 112 |
+
.kpi-header {
|
| 113 |
+
display: flex;
|
| 114 |
+
align-items: center;
|
| 115 |
+
justify-content: space-between;
|
| 116 |
+
gap: 8px;
|
| 117 |
+
}
|
| 118 |
+
.kpi-icon {
|
| 119 |
+
width: 32px;
|
| 120 |
+
height: 32px;
|
| 121 |
+
padding: 6px;
|
| 122 |
+
border-radius: 6px;
|
| 123 |
+
display: flex;
|
| 124 |
+
align-items: center;
|
| 125 |
+
justify-content: center;
|
| 126 |
+
flex-shrink: 0;
|
| 127 |
+
}
|
| 128 |
+
.kpi-icon svg {
|
| 129 |
+
width: 20px;
|
| 130 |
+
height: 20px;
|
| 131 |
+
stroke-width: 2;
|
| 132 |
+
}
|
| 133 |
+
.kpi-icon.blue { background: #dbeafe; color: #2563eb; }
|
| 134 |
+
.kpi-icon.green { background: #dcfce7; color: #16a34a; }
|
| 135 |
+
.kpi-icon.orange { background: #fed7aa; color: #ea580c; }
|
| 136 |
+
.kpi-icon.purple { background: #e9d5ff; color: #9333ea; }
|
| 137 |
+
.kpi-label {
|
| 138 |
+
font-size: 11px;
|
| 139 |
+
color: var(--muted);
|
| 140 |
+
letter-spacing: 0.3px;
|
| 141 |
+
text-transform: uppercase;
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
flex: 1;
|
| 144 |
+
}
|
| 145 |
+
.kpi-value {
|
| 146 |
+
font-size: 24px;
|
| 147 |
+
font-weight: 700;
|
| 148 |
+
color: var(--text);
|
| 149 |
+
line-height: 1.2;
|
| 150 |
+
}
|
| 151 |
+
.kpi-trend {
|
| 152 |
+
font-size: 11px;
|
| 153 |
+
color: var(--muted);
|
| 154 |
+
display: flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
gap: 4px;
|
| 157 |
+
}
|
| 158 |
+
.tabs {
|
| 159 |
+
background: var(--card);
|
| 160 |
+
border-radius: 10px;
|
| 161 |
+
padding: 8px;
|
| 162 |
+
display: flex;
|
| 163 |
+
gap: 6px;
|
| 164 |
+
overflow-x: auto;
|
| 165 |
+
box-shadow: 0 1px 3px rgba(15,23,42,0.08);
|
| 166 |
+
}
|
| 167 |
+
.tab {
|
| 168 |
+
border: none;
|
| 169 |
+
background: transparent;
|
| 170 |
+
padding: 8px 16px;
|
| 171 |
+
border-radius: 6px;
|
| 172 |
+
font-weight: 600;
|
| 173 |
+
color: var(--muted);
|
| 174 |
+
cursor: pointer;
|
| 175 |
+
}
|
| 176 |
+
.tab.active { background: var(--primary); color: #fff; }
|
| 177 |
+
.tab-content { margin-top: 16px; display: none; }
|
| 178 |
+
.tab-content.active { display: block; }
|
| 179 |
+
.card {
|
| 180 |
+
background: var(--card);
|
| 181 |
+
border-radius: 12px;
|
| 182 |
+
padding: 20px;
|
| 183 |
+
box-shadow: 0 1px 3px rgba(15,23,42,0.08);
|
| 184 |
+
border: 1px solid var(--border);
|
| 185 |
+
margin-bottom: 16px;
|
| 186 |
+
transition: all 0.2s ease;
|
| 187 |
+
}
|
| 188 |
+
.card:hover {
|
| 189 |
+
box-shadow: 0 4px 12px rgba(15,23,42,0.12);
|
| 190 |
+
border-color: rgba(37,99,235,0.2);
|
| 191 |
+
}
|
| 192 |
+
.card-header {
|
| 193 |
+
display: flex;
|
| 194 |
+
justify-content: space-between;
|
| 195 |
+
align-items: center;
|
| 196 |
+
margin-bottom: 16px;
|
| 197 |
+
padding-bottom: 12px;
|
| 198 |
+
border-bottom: 2px solid var(--border);
|
| 199 |
+
}
|
| 200 |
+
.card-title {
|
| 201 |
+
font-size: 16px;
|
| 202 |
+
font-weight: 700;
|
| 203 |
+
color: var(--text);
|
| 204 |
+
display: flex;
|
| 205 |
+
align-items: center;
|
| 206 |
+
gap: 8px;
|
| 207 |
+
}
|
| 208 |
+
.grid { display: grid; gap: 16px; }
|
| 209 |
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
|
| 210 |
+
.table-wrapper {
|
| 211 |
+
overflow-x: auto;
|
| 212 |
+
border-radius: 8px;
|
| 213 |
+
border: 1px solid var(--border);
|
| 214 |
+
}
|
| 215 |
+
table {
|
| 216 |
+
width: 100%;
|
| 217 |
+
border-collapse: separate;
|
| 218 |
+
border-spacing: 0;
|
| 219 |
+
font-size: 13px;
|
| 220 |
+
}
|
| 221 |
+
thead {
|
| 222 |
+
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
| 223 |
+
position: sticky;
|
| 224 |
+
top: 0;
|
| 225 |
+
z-index: 10;
|
| 226 |
+
}
|
| 227 |
+
thead th {
|
| 228 |
+
padding: 14px 16px;
|
| 229 |
+
text-align: left;
|
| 230 |
+
font-weight: 700;
|
| 231 |
+
font-size: 11px;
|
| 232 |
+
text-transform: uppercase;
|
| 233 |
+
letter-spacing: 0.5px;
|
| 234 |
+
color: var(--muted);
|
| 235 |
+
border-bottom: 2px solid var(--border);
|
| 236 |
+
white-space: nowrap;
|
| 237 |
+
}
|
| 238 |
+
thead th:first-child { border-top-left-radius: 8px; }
|
| 239 |
+
thead th:last-child { border-top-right-radius: 8px; }
|
| 240 |
+
tbody tr {
|
| 241 |
+
transition: all 0.15s ease;
|
| 242 |
+
border-bottom: 1px solid #f3f4f6;
|
| 243 |
+
}
|
| 244 |
+
tbody tr:hover {
|
| 245 |
+
background: #f9fafb;
|
| 246 |
+
transform: scale(1.001);
|
| 247 |
+
box-shadow: 0 2px 8px rgba(37,99,235,0.08);
|
| 248 |
+
}
|
| 249 |
+
tbody tr:last-child td:first-child { border-bottom-left-radius: 8px; }
|
| 250 |
+
tbody tr:last-child td:last-child { border-bottom-right-radius: 8px; }
|
| 251 |
+
tbody td {
|
| 252 |
+
padding: 14px 16px;
|
| 253 |
+
color: var(--text);
|
| 254 |
+
vertical-align: middle;
|
| 255 |
+
}
|
| 256 |
+
tbody td strong {
|
| 257 |
+
font-weight: 600;
|
| 258 |
+
color: var(--text);
|
| 259 |
+
}
|
| 260 |
+
tbody td:first-child {
|
| 261 |
+
font-weight: 600;
|
| 262 |
+
}
|
| 263 |
+
.badge {
|
| 264 |
+
padding: 5px 12px;
|
| 265 |
+
border-radius: 6px;
|
| 266 |
+
font-size: 11px;
|
| 267 |
+
font-weight: 700;
|
| 268 |
+
display: inline-flex;
|
| 269 |
+
align-items: center;
|
| 270 |
+
gap: 5px;
|
| 271 |
+
text-transform: uppercase;
|
| 272 |
+
letter-spacing: 0.3px;
|
| 273 |
+
border: 1px solid transparent;
|
| 274 |
+
transition: all 0.2s ease;
|
| 275 |
+
}
|
| 276 |
+
.badge.success {
|
| 277 |
+
background: #dcfce7;
|
| 278 |
+
color: #15803d;
|
| 279 |
+
border-color: #86efac;
|
| 280 |
+
}
|
| 281 |
+
.badge.warn {
|
| 282 |
+
background: #fef3c7;
|
| 283 |
+
color: #d97706;
|
| 284 |
+
border-color: #fde047;
|
| 285 |
+
}
|
| 286 |
+
.badge.danger {
|
| 287 |
+
background: #fee2e2;
|
| 288 |
+
color: #b91c1c;
|
| 289 |
+
border-color: #fca5a5;
|
| 290 |
+
}
|
| 291 |
+
.badge.info {
|
| 292 |
+
background: #dbeafe;
|
| 293 |
+
color: #1e40af;
|
| 294 |
+
border-color: #93c5fd;
|
| 295 |
+
}
|
| 296 |
+
.list {
|
| 297 |
+
display: flex;
|
| 298 |
+
flex-direction: column;
|
| 299 |
+
gap: 10px;
|
| 300 |
+
}
|
| 301 |
+
.list-item {
|
| 302 |
+
padding: 12px;
|
| 303 |
+
border-radius: 8px;
|
| 304 |
+
border: 1px solid var(--border);
|
| 305 |
+
background: var(--card);
|
| 306 |
+
transition: all 0.15s ease;
|
| 307 |
+
}
|
| 308 |
+
.list-item:hover {
|
| 309 |
+
background: #f9fafb;
|
| 310 |
+
border-color: var(--primary);
|
| 311 |
+
transform: translateX(2px);
|
| 312 |
+
box-shadow: 0 2px 8px rgba(37,99,235,0.1);
|
| 313 |
+
}
|
| 314 |
+
.list-item:last-child {
|
| 315 |
+
margin-bottom: 0;
|
| 316 |
+
}
|
| 317 |
+
.resource-search { display: flex; gap: 12px; }
|
| 318 |
+
.resource-search input {
|
| 319 |
+
flex: 1;
|
| 320 |
+
padding: 10px 12px;
|
| 321 |
+
border-radius: 6px;
|
| 322 |
+
border: 1px solid var(--border);
|
| 323 |
+
font-size: 14px;
|
| 324 |
+
}
|
| 325 |
+
.resource-columns {
|
| 326 |
+
display: grid;
|
| 327 |
+
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
| 328 |
+
gap: 12px;
|
| 329 |
+
margin-top: 16px;
|
| 330 |
+
}
|
| 331 |
+
.resource-item {
|
| 332 |
+
padding: 10px;
|
| 333 |
+
border: 1px solid var(--border);
|
| 334 |
+
border-radius: 6px;
|
| 335 |
+
margin-bottom: 8px;
|
| 336 |
+
font-size: 13px;
|
| 337 |
+
}
|
| 338 |
+
.form-grid {
|
| 339 |
+
display: grid;
|
| 340 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 341 |
+
gap: 10px;
|
| 342 |
+
}
|
| 343 |
+
label { font-size: 12px; font-weight: 600; color: var(--muted); }
|
| 344 |
+
input, textarea, select {
|
| 345 |
+
width: 100%;
|
| 346 |
+
padding: 10px 14px;
|
| 347 |
+
border: 1px solid var(--border);
|
| 348 |
+
border-radius: 8px;
|
| 349 |
+
font-size: 14px;
|
| 350 |
+
font-family: inherit;
|
| 351 |
+
background: var(--card);
|
| 352 |
+
color: var(--text);
|
| 353 |
+
transition: all 0.2s ease;
|
| 354 |
+
}
|
| 355 |
+
input:focus, textarea:focus, select:focus {
|
| 356 |
+
outline: none;
|
| 357 |
+
border-color: var(--primary);
|
| 358 |
+
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
|
| 359 |
+
}
|
| 360 |
+
input:hover, textarea:hover, select:hover {
|
| 361 |
+
border-color: rgba(37,99,235,0.3);
|
| 362 |
+
}
|
| 363 |
+
textarea {
|
| 364 |
+
min-height: 120px;
|
| 365 |
+
resize: vertical;
|
| 366 |
+
font-family: 'Monaco', 'Courier New', monospace;
|
| 367 |
+
font-size: 13px;
|
| 368 |
+
}
|
| 369 |
+
.toast-container {
|
| 370 |
+
position: fixed;
|
| 371 |
+
bottom: 20px;
|
| 372 |
+
right: 20px;
|
| 373 |
+
display: flex;
|
| 374 |
+
flex-direction: column;
|
| 375 |
+
gap: 10px;
|
| 376 |
+
}
|
| 377 |
+
.toast {
|
| 378 |
+
background: #111827;
|
| 379 |
+
color: white;
|
| 380 |
+
padding: 12px 16px;
|
| 381 |
+
border-radius: 6px;
|
| 382 |
+
min-width: 240px;
|
| 383 |
+
box-shadow: 0 8px 20px rgba(15,23,42,0.25);
|
| 384 |
+
}
|
| 385 |
+
@media (max-width: 720px) {
|
| 386 |
+
.kpi-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
|
| 387 |
+
.grid-2 { grid-template-columns: 1fr; }
|
| 388 |
+
.resource-search { flex-direction: column; }
|
| 389 |
+
}
|
| 390 |
+
</style>
|
| 391 |
+
</head>
|
| 392 |
+
<body>
|
| 393 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 394 |
+
<div class="container">
|
| 395 |
+
<div class="header">
|
| 396 |
+
<div class="logo">
|
| 397 |
+
<h1>🚀 Crypto API Monitor</h1>
|
| 398 |
+
<div style="font-size:12px; color:var(--muted);">Real API diagnostics + market intelligence</div>
|
| 399 |
+
</div>
|
| 400 |
+
<div class="header-actions">
|
| 401 |
+
<div class="status-pill" id="wsStatus">Connecting...</div>
|
| 402 |
+
<button class="btn secondary" onclick="refreshAll()">Refresh</button>
|
| 403 |
+
<button class="btn" onclick="loadErrorSummary()">Diagnostics</button>
|
| 404 |
+
</div>
|
| 405 |
+
</div>
|
| 406 |
+
|
| 407 |
+
<div class="kpi-grid">
|
| 408 |
+
<div class="kpi-card">
|
| 409 |
+
<div class="kpi-header">
|
| 410 |
+
<div class="kpi-label">Total APIs</div>
|
| 411 |
+
<div class="kpi-icon blue">
|
| 412 |
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 413 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
| 414 |
+
</svg>
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
<div class="kpi-value" id="kpiTotalAPIs">--</div>
|
| 418 |
+
<div class="kpi-trend" id="kpiTotalTrend">Loading…</div>
|
| 419 |
+
</div>
|
| 420 |
+
<div class="kpi-card">
|
| 421 |
+
<div class="kpi-header">
|
| 422 |
+
<div class="kpi-label">Online</div>
|
| 423 |
+
<div class="kpi-icon green">
|
| 424 |
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 425 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
| 426 |
+
</svg>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
<div class="kpi-value" id="kpiOnline">--</div>
|
| 430 |
+
<div class="kpi-trend" id="kpiOnlineTrend">Loading…</div>
|
| 431 |
+
</div>
|
| 432 |
+
<div class="kpi-card">
|
| 433 |
+
<div class="kpi-header">
|
| 434 |
+
<div class="kpi-label">Avg Response</div>
|
| 435 |
+
<div class="kpi-icon orange">
|
| 436 |
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 437 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
| 438 |
+
</svg>
|
| 439 |
+
</div>
|
| 440 |
+
</div>
|
| 441 |
+
<div class="kpi-value" id="kpiAvgResponse">--</div>
|
| 442 |
+
<div class="kpi-trend" id="kpiResponseTrend">Loading…</div>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="kpi-card">
|
| 445 |
+
<div class="kpi-header">
|
| 446 |
+
<div class="kpi-label">Last Update</div>
|
| 447 |
+
<div class="kpi-icon purple">
|
| 448 |
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 449 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
| 450 |
+
</svg>
|
| 451 |
+
</div>
|
| 452 |
+
</div>
|
| 453 |
+
<div class="kpi-value" id="kpiLastUpdate" style="font-size:16px;">--</div>
|
| 454 |
+
<div class="kpi-trend">Auto-refresh</div>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
|
| 458 |
+
<div class="tabs" id="tabBar">
|
| 459 |
+
<button class="tab active" data-tab="dashboard" onclick="switchTab(event, 'dashboard')">Dashboard</button>
|
| 460 |
+
<button class="tab" data-tab="providers" onclick="switchTab(event, 'providers')">Providers</button>
|
| 461 |
+
<button class="tab" data-tab="market" onclick="switchTab(event, 'market')">Market</button>
|
| 462 |
+
<button class="tab" data-tab="sentiment" onclick="switchTab(event, 'sentiment')">Sentiment</button>
|
| 463 |
+
<button class="tab" data-tab="news" onclick="switchTab(event, 'news')">News</button>
|
| 464 |
+
<button class="tab" data-tab="resources" onclick="switchTab(event, 'resources')">Resources & Tools</button>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<div class="tab-content active" id="tab-dashboard">
|
| 468 |
+
<div class="grid grid-2">
|
| 469 |
+
<div class="card">
|
| 470 |
+
<div class="card-header">
|
| 471 |
+
<h3 class="card-title">Provider Overview</h3>
|
| 472 |
+
<button class="btn secondary" onclick="loadProviders()">Reload</button>
|
| 473 |
+
</div>
|
| 474 |
+
<div class="table-wrapper" style="overflow:auto;">
|
| 475 |
+
<table>
|
| 476 |
+
<thead>
|
| 477 |
+
<tr><th>Provider</th><th>Status</th><th>Response</th><th>Uptime</th></tr>
|
| 478 |
+
</thead>
|
| 479 |
+
<tbody id="providersTableBody">
|
| 480 |
+
<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">Loading providers…</td></tr>
|
| 481 |
+
</tbody>
|
| 482 |
+
</table>
|
| 483 |
+
</div>
|
| 484 |
+
</div>
|
| 485 |
+
<div class="card">
|
| 486 |
+
<div class="card-header">
|
| 487 |
+
<h3 class="card-title">Error Monitor</h3>
|
| 488 |
+
<button class="btn secondary" onclick="loadErrorSummary()">Refresh</button>
|
| 489 |
+
</div>
|
| 490 |
+
<div id="errorSummaryCard" style="font-size:13px; color:var(--muted);">Gathering diagnostics…</div>
|
| 491 |
+
<div id="diagnosticsList" class="list" style="margin-top:12px;"></div>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
<div class="grid grid-2">
|
| 495 |
+
<div class="card">
|
| 496 |
+
<div class="card-header"><h3 class="card-title">Health Trend</h3></div>
|
| 497 |
+
<div style="height:260px;"><canvas id="healthChart"></canvas></div>
|
| 498 |
+
</div>
|
| 499 |
+
<div class="card">
|
| 500 |
+
<div class="card-header"><h3 class="card-title">Status Distribution</h3></div>
|
| 501 |
+
<div style="height:260px;"><canvas id="statusChart"></canvas></div>
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
|
| 506 |
+
<div class="tab-content" id="tab-providers">
|
| 507 |
+
<div class="card">
|
| 508 |
+
<div class="card-header">
|
| 509 |
+
<h3 class="card-title">Providers Detail</h3>
|
| 510 |
+
<button class="btn secondary" onclick="loadProviders()">Reload</button>
|
| 511 |
+
</div>
|
| 512 |
+
<div id="providersDetail"></div>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<div class="tab-content" id="tab-market">
|
| 517 |
+
<div class="grid grid-2">
|
| 518 |
+
<div class="card">
|
| 519 |
+
<div class="card-header"><h3 class="card-title">Global Stats</h3></div>
|
| 520 |
+
<div id="marketGlobalStats" class="list"></div>
|
| 521 |
+
</div>
|
| 522 |
+
<div class="card">
|
| 523 |
+
<div class="card-header"><h3 class="card-title">Top Movers</h3></div>
|
| 524 |
+
<div style="height:260px;"><canvas id="marketChart"></canvas></div>
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
<div class="grid grid-2">
|
| 528 |
+
<div class="card">
|
| 529 |
+
<div class="card-header"><h3 class="card-title">Top Assets</h3></div>
|
| 530 |
+
<div class="table-wrapper" style="overflow:auto; max-height:320px;">
|
| 531 |
+
<table>
|
| 532 |
+
<thead><tr><th>Rank</th><th>Name</th><th>Price</th><th>24h</th><th>Market Cap</th></tr></thead>
|
| 533 |
+
<tbody id="marketTableBody"></tbody>
|
| 534 |
+
</table>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
<div class="card">
|
| 538 |
+
<div class="card-header"><h3 class="card-title">Trending Now</h3></div>
|
| 539 |
+
<div id="trendingList" class="list"></div>
|
| 540 |
+
</div>
|
| 541 |
+
</div>
|
| 542 |
+
</div>
|
| 543 |
+
|
| 544 |
+
<div class="tab-content" id="tab-sentiment">
|
| 545 |
+
<div class="grid grid-2">
|
| 546 |
+
<div class="card">
|
| 547 |
+
<div class="card-header"><h3 class="card-title">Fear & Greed Index</h3></div>
|
| 548 |
+
<div id="sentimentCard" style="font-size:14px; color:var(--muted);">Loading sentiment…</div>
|
| 549 |
+
</div>
|
| 550 |
+
<div class="card">
|
| 551 |
+
<div class="card-header"><h3 class="card-title">DeFi TVL</h3></div>
|
| 552 |
+
<div class="table-wrapper" style="overflow:auto; max-height:280px;">
|
| 553 |
+
<table>
|
| 554 |
+
<thead><tr><th>Protocol</th><th>TVL</th><th>24h</th><th>Chain</th></tr></thead>
|
| 555 |
+
<tbody id="defiTableBody"></tbody>
|
| 556 |
+
</table>
|
| 557 |
+
</div>
|
| 558 |
+
</div>
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
|
| 562 |
+
<div class="tab-content" id="tab-news">
|
| 563 |
+
<div class="card">
|
| 564 |
+
<div class="card-header">
|
| 565 |
+
<h3 class="card-title">Latest Headlines</h3>
|
| 566 |
+
<button class="btn secondary" onclick="loadNews()">Reload</button>
|
| 567 |
+
</div>
|
| 568 |
+
<div id="newsList" class="list"></div>
|
| 569 |
+
</div>
|
| 570 |
+
</div>
|
| 571 |
+
|
| 572 |
+
<div class="tab-content" id="tab-resources">
|
| 573 |
+
<div class="card">
|
| 574 |
+
<div class="card-header">
|
| 575 |
+
<h3 class="card-title">Resource Search</h3>
|
| 576 |
+
<span style="font-size:12px;color:var(--muted);">Live search across providers + HuggingFace registry</span>
|
| 577 |
+
</div>
|
| 578 |
+
<div class="resource-search">
|
| 579 |
+
<input type="text" id="resourceSearch" placeholder="Search provider, model or dataset..." />
|
| 580 |
+
<select id="resourceFilter" onchange="loadResourceSearch()">
|
| 581 |
+
<option value="all">All sources</option>
|
| 582 |
+
<option value="providers">Providers</option>
|
| 583 |
+
<option value="models">Models</option>
|
| 584 |
+
<option value="datasets">Datasets</option>
|
| 585 |
+
</select>
|
| 586 |
+
</div>
|
| 587 |
+
<div class="resource-columns">
|
| 588 |
+
<div>
|
| 589 |
+
<h4>Providers <span id="resourceCountProviders" style="color:var(--muted);"></span></h4>
|
| 590 |
+
<div id="resourceResultsProviders"></div>
|
| 591 |
+
</div>
|
| 592 |
+
<div>
|
| 593 |
+
<h4>Models <span id="resourceCountModels" style="color:var(--muted);"></span></h4>
|
| 594 |
+
<div id="resourceResultsModels"></div>
|
| 595 |
+
</div>
|
| 596 |
+
<div>
|
| 597 |
+
<h4>Datasets <span id="resourceCountDatasets" style="color:var(--muted);"></span></h4>
|
| 598 |
+
<div id="resourceResultsDatasets"></div>
|
| 599 |
+
</div>
|
| 600 |
+
</div>
|
| 601 |
+
</div>
|
| 602 |
+
<div class="grid grid-2">
|
| 603 |
+
<div class="card">
|
| 604 |
+
<div class="card-header"><h3 class="card-title">Export & Backup</h3></div>
|
| 605 |
+
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
| 606 |
+
<button class="btn" onclick="handleExport('json')">Export JSON Snapshot</button>
|
| 607 |
+
<button class="btn" onclick="handleExport('csv')">Export CSV</button>
|
| 608 |
+
<button class="btn secondary" onclick="handleBackup()">Create Backup</button>
|
| 609 |
+
</div>
|
| 610 |
+
<div id="exportHistory" class="list" style="margin-top:12px;"></div>
|
| 611 |
+
</div>
|
| 612 |
+
<div class="card">
|
| 613 |
+
<div class="card-header"><h3 class="card-title">Import Provider</h3></div>
|
| 614 |
+
<form id="importForm" onsubmit="handleImportSingle(event)">
|
| 615 |
+
<div class="form-grid">
|
| 616 |
+
<div><label>Name</label><input name="name" required></div>
|
| 617 |
+
<div><label>Category</label><input name="category" required></div>
|
| 618 |
+
<div><label>Endpoint URL</label><input name="endpoint_url" required></div>
|
| 619 |
+
<div><label>Health Endpoint</label><input name="health_check_endpoint"></div>
|
| 620 |
+
<div><label>Rate Limit</label><input name="rate_limit"></div>
|
| 621 |
+
<div><label>Timeout (ms)</label><input name="timeout_ms" type="number" value="10000"></div>
|
| 622 |
+
</div>
|
| 623 |
+
<label style="margin-top:12px; display:block;">Notes<textarea name="notes"></textarea></label>
|
| 624 |
+
<div style="margin-top:12px; display:flex; gap:10px; align-items:center;">
|
| 625 |
+
<label style="display:flex; gap:6px; align-items:center; font-size:13px;">
|
| 626 |
+
<input type="checkbox" name="requires_key"> Requires API Key
|
| 627 |
+
</label>
|
| 628 |
+
<input name="api_key" placeholder="API Key (optional)" style="flex:1;">
|
| 629 |
+
</div>
|
| 630 |
+
<button class="btn" style="margin-top:12px;" type="submit">Import Provider</button>
|
| 631 |
+
</form>
|
| 632 |
+
<hr style="margin:20px 0; border:none; border-top:1px solid var(--border);">
|
| 633 |
+
<label style="display:block; font-size:13px; color:var(--muted); margin-bottom:6px;">Bulk JSON Import</label>
|
| 634 |
+
<textarea id="bulkImportTextarea" placeholder='[{"name":"Sample API","category":"market","endpoint_url":"https://..."}]'></textarea>
|
| 635 |
+
<button class="btn secondary" style="margin-top:10px;" onclick="handleImportBulk()">Run Bulk Import</button>
|
| 636 |
+
</div>
|
| 637 |
+
</div>
|
| 638 |
+
</div>
|
| 639 |
+
</div>
|
| 640 |
+
|
| 641 |
+
<script>
|
| 642 |
+
const config = {
|
| 643 |
+
apiBaseUrl: '',
|
| 644 |
+
wsUrl: (() => {
|
| 645 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 646 |
+
return `${protocol}//${window.location.host}/ws`;
|
| 647 |
+
})(),
|
| 648 |
+
autoRefreshInterval: 30000
|
| 649 |
+
};
|
| 650 |
+
|
| 651 |
+
const state = {
|
| 652 |
+
ws: null,
|
| 653 |
+
wsConnected: false,
|
| 654 |
+
providers: [],
|
| 655 |
+
market: { cryptocurrencies: [], global: {} },
|
| 656 |
+
trending: [],
|
| 657 |
+
sentiment: null,
|
| 658 |
+
defi: [],
|
| 659 |
+
news: [],
|
| 660 |
+
errorSummary: null,
|
| 661 |
+
diagnostics: null,
|
| 662 |
+
resources: { providers: [], models: [], datasets: [] },
|
| 663 |
+
exports: [],
|
| 664 |
+
charts: { health: null, status: null, market: null },
|
| 665 |
+
currentTab: 'dashboard',
|
| 666 |
+
resourceSearchTimeout: null,
|
| 667 |
+
lastUpdate: null
|
| 668 |
+
};
|
| 669 |
+
|
| 670 |
+
function showToast(message, type = 'info') {
|
| 671 |
+
const container = document.getElementById('toastContainer');
|
| 672 |
+
const toast = document.createElement('div');
|
| 673 |
+
toast.className = 'toast';
|
| 674 |
+
toast.style.background = type === 'error' ? '#b91c1c' : (type === 'success' ? '#065f46' : '#111827');
|
| 675 |
+
toast.textContent = message;
|
| 676 |
+
container.appendChild(toast);
|
| 677 |
+
setTimeout(() => toast.remove(), 3000);
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
async function apiCall(endpoint, options = {}) {
|
| 681 |
+
const response = await fetch(config.apiBaseUrl + endpoint, options);
|
| 682 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 683 |
+
return await response.json();
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
function initializeWebSocket() {
|
| 687 |
+
try {
|
| 688 |
+
state.ws = new WebSocket(config.wsUrl);
|
| 689 |
+
state.ws.onopen = () => {
|
| 690 |
+
state.wsConnected = true;
|
| 691 |
+
const pill = document.getElementById('wsStatus');
|
| 692 |
+
pill.textContent = 'Connected';
|
| 693 |
+
pill.style.background = '#dcfce7';
|
| 694 |
+
pill.style.color = '#15803d';
|
| 695 |
+
};
|
| 696 |
+
state.ws.onclose = () => {
|
| 697 |
+
state.wsConnected = false;
|
| 698 |
+
const pill = document.getElementById('wsStatus');
|
| 699 |
+
pill.textContent = 'Disconnected';
|
| 700 |
+
pill.style.background = '#fee2e2';
|
| 701 |
+
pill.style.color = '#b91c1c';
|
| 702 |
+
};
|
| 703 |
+
state.ws.onmessage = (event) => {
|
| 704 |
+
const data = JSON.parse(event.data);
|
| 705 |
+
if (data.type === 'status_update' && data.data?.providers) {
|
| 706 |
+
updateKPIs(data.data.providers);
|
| 707 |
+
}
|
| 708 |
+
if (data.type === 'market_update' && Array.isArray(data.data)) {
|
| 709 |
+
state.market.cryptocurrencies = data.data;
|
| 710 |
+
renderMarketTable();
|
| 711 |
+
updateMarketChart();
|
| 712 |
+
}
|
| 713 |
+
if (data.type === 'sentiment_update') {
|
| 714 |
+
state.sentiment = data.data;
|
| 715 |
+
renderSentiment();
|
| 716 |
+
}
|
| 717 |
+
};
|
| 718 |
+
} catch (err) {
|
| 719 |
+
console.error('WebSocket error', err);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
async function loadInitialData() {
|
| 724 |
+
try {
|
| 725 |
+
await Promise.all([
|
| 726 |
+
loadProviders(),
|
| 727 |
+
loadMarket(),
|
| 728 |
+
loadTrending(),
|
| 729 |
+
loadSentimentData(),
|
| 730 |
+
loadDefi(),
|
| 731 |
+
loadErrorSummary(),
|
| 732 |
+
loadNews(),
|
| 733 |
+
loadResourceSearch()
|
| 734 |
+
]);
|
| 735 |
+
initializeCharts();
|
| 736 |
+
state.lastUpdate = new Date();
|
| 737 |
+
updateLastUpdateDisplay();
|
| 738 |
+
showToast('Dashboard ready', 'success');
|
| 739 |
+
} catch (err) {
|
| 740 |
+
console.error(err);
|
| 741 |
+
showToast('Failed to load initial data', 'error');
|
| 742 |
+
}
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
async function loadProviders() {
|
| 746 |
+
try {
|
| 747 |
+
const data = await apiCall('/api/providers');
|
| 748 |
+
state.providers = Array.isArray(data) ? data : [];
|
| 749 |
+
renderProvidersTable();
|
| 750 |
+
renderProvidersDetail();
|
| 751 |
+
updateStatusChart();
|
| 752 |
+
updateKPIs(state.providers);
|
| 753 |
+
} catch (err) {
|
| 754 |
+
console.error(err);
|
| 755 |
+
document.getElementById('providersTableBody').innerHTML = `<tr><td colspan="4" style="padding:40px; text-align:center;">${err.message}</td></tr>`;
|
| 756 |
+
}
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
function renderProvidersTable() {
|
| 760 |
+
const tbody = document.getElementById('providersTableBody');
|
| 761 |
+
if (!state.providers.length) {
|
| 762 |
+
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No providers available</td></tr>';
|
| 763 |
+
return;
|
| 764 |
+
}
|
| 765 |
+
tbody.innerHTML = state.providers.slice(0, 8).map(p => `
|
| 766 |
+
<tr>
|
| 767 |
+
<td>
|
| 768 |
+
<div style="font-weight:600;">${p.name || 'Unknown'}</div>
|
| 769 |
+
<div style="font-size:11px;color:var(--muted);">${p.category || 'general'}</div>
|
| 770 |
+
</td>
|
| 771 |
+
<td>${renderStatusBadge(p.status)}</td>
|
| 772 |
+
<td>${p.response_time_ms ? `${p.response_time_ms}ms` : '--'}</td>
|
| 773 |
+
<td>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</td>
|
| 774 |
+
</tr>
|
| 775 |
+
`).join('');
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
function renderProvidersDetail() {
|
| 779 |
+
const container = document.getElementById('providersDetail');
|
| 780 |
+
if (!state.providers.length) {
|
| 781 |
+
container.innerHTML = '<div style="padding:40px; text-align:center; color:var(--muted);">No providers data available</div>';
|
| 782 |
+
return;
|
| 783 |
+
}
|
| 784 |
+
container.innerHTML = `
|
| 785 |
+
<div class="table-wrapper">
|
| 786 |
+
<table>
|
| 787 |
+
<thead>
|
| 788 |
+
<tr>
|
| 789 |
+
<th>Name</th>
|
| 790 |
+
<th>Status</th>
|
| 791 |
+
<th>Response Time</th>
|
| 792 |
+
<th>Success Rate</th>
|
| 793 |
+
<th>Rate Limit</th>
|
| 794 |
+
</tr>
|
| 795 |
+
</thead>
|
| 796 |
+
<tbody>
|
| 797 |
+
${state.providers.map(p => `
|
| 798 |
+
<tr>
|
| 799 |
+
<td>
|
| 800 |
+
<strong>${p.name || 'Unknown'}</strong>
|
| 801 |
+
<div style="font-size:11px;color:var(--muted);margin-top:2px;">${p.category || 'general'}</div>
|
| 802 |
+
</td>
|
| 803 |
+
<td>${renderStatusBadge(p.status)}</td>
|
| 804 |
+
<td><strong>${p.avg_response_time_ms ? `${p.avg_response_time_ms}ms` : (p.response_time_ms ? `${p.response_time_ms}ms` : '--')}</strong></td>
|
| 805 |
+
<td><strong>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</strong></td>
|
| 806 |
+
<td style="font-size:12px;color:var(--muted);">${p.rate_limit || '—'}</td>
|
| 807 |
+
</tr>`).join('')}
|
| 808 |
+
</tbody>
|
| 809 |
+
</table>
|
| 810 |
+
</div>`;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
function renderStatusBadge(status = 'unknown') {
|
| 814 |
+
const normalized = (status || '').toLowerCase();
|
| 815 |
+
let cls = 'badge warn';
|
| 816 |
+
if (['online', 'healthy'].includes(normalized)) cls = 'badge success';
|
| 817 |
+
if (['offline', 'error'].includes(normalized)) cls = 'badge danger';
|
| 818 |
+
return `<span class="${cls}">${status || 'unknown'}</span>`;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
function updateKPIs(data) {
|
| 822 |
+
const providers = Array.isArray(data) ? data : (data?.providers || []);
|
| 823 |
+
const total = providers.length;
|
| 824 |
+
const online = providers.filter(p => ['online', 'healthy'].includes((p.status || '').toLowerCase())).length;
|
| 825 |
+
const responseTimes = providers.map(p => p.response_time_ms || p.avg_response_time_ms).filter(Boolean);
|
| 826 |
+
const avgResponse = responseTimes.length ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0;
|
| 827 |
+
|
| 828 |
+
document.getElementById('kpiTotalAPIs').textContent = total;
|
| 829 |
+
document.getElementById('kpiTotalTrend').textContent = `${total} tracked providers`;
|
| 830 |
+
document.getElementById('kpiOnline').textContent = online;
|
| 831 |
+
document.getElementById('kpiOnlineTrend').textContent = total ? `${Math.round((online / total) * 100)}% uptime` : 'No data';
|
| 832 |
+
document.getElementById('kpiAvgResponse').textContent = avgResponse ? `${avgResponse}ms` : '--';
|
| 833 |
+
document.getElementById('kpiResponseTrend').textContent = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
|
| 834 |
+
updateHealthChart(avgResponse);
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
function updateLastUpdateDisplay() {
|
| 838 |
+
if (!state.lastUpdate) return;
|
| 839 |
+
document.getElementById('kpiLastUpdate').textContent = state.lastUpdate.toLocaleTimeString();
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
function initializeCharts() {
|
| 843 |
+
const healthCtx = document.getElementById('healthChart').getContext('2d');
|
| 844 |
+
const statusCtx = document.getElementById('statusChart').getContext('2d');
|
| 845 |
+
const marketCtx = document.getElementById('marketChart').getContext('2d');
|
| 846 |
+
|
| 847 |
+
if (state.charts.health) state.charts.health.destroy();
|
| 848 |
+
if (state.charts.status) state.charts.status.destroy();
|
| 849 |
+
if (state.charts.market) state.charts.market.destroy();
|
| 850 |
+
|
| 851 |
+
state.charts.health = new Chart(healthCtx, {
|
| 852 |
+
type: 'line',
|
| 853 |
+
data: { labels: [], datasets: [{ label: 'Avg Response (ms)', data: [], borderColor: '#2563eb', fill: false }] },
|
| 854 |
+
options: { responsive: true, maintainAspectRatio: false }
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
state.charts.status = new Chart(statusCtx, {
|
| 858 |
+
type: 'doughnut',
|
| 859 |
+
data: { labels: ['Online', 'Degraded', 'Offline'], datasets: [{ data: [0, 0, 0], backgroundColor: ['#16a34a', '#fcd34d', '#f87171'] }] },
|
| 860 |
+
options: { responsive: true, maintainAspectRatio: false }
|
| 861 |
+
});
|
| 862 |
+
|
| 863 |
+
state.charts.market = new Chart(marketCtx, {
|
| 864 |
+
type: 'bar',
|
| 865 |
+
data: { labels: [], datasets: [{ label: 'Market Cap (B USD)', data: [], backgroundColor: '#93c5fd' }] },
|
| 866 |
+
options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } }
|
| 867 |
+
});
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
function updateHealthChart(value) {
|
| 871 |
+
if (!state.charts.health || !value) return;
|
| 872 |
+
const chart = state.charts.health;
|
| 873 |
+
chart.data.labels.push(new Date().toLocaleTimeString());
|
| 874 |
+
chart.data.datasets[0].data.push(value);
|
| 875 |
+
if (chart.data.labels.length > 12) {
|
| 876 |
+
chart.data.labels.shift();
|
| 877 |
+
chart.data.datasets[0].data.shift();
|
| 878 |
+
}
|
| 879 |
+
chart.update();
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
function updateStatusChart() {
|
| 883 |
+
if (!state.charts.status) return;
|
| 884 |
+
const online = state.providers.filter(p => (p.status || '').toLowerCase() === 'online').length;
|
| 885 |
+
const degraded = state.providers.filter(p => (p.status || '').toLowerCase() === 'degraded').length;
|
| 886 |
+
const offline = state.providers.length - online - degraded;
|
| 887 |
+
state.charts.status.data.datasets[0].data = [online, degraded, offline];
|
| 888 |
+
state.charts.status.update();
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
async function loadMarket() {
|
| 892 |
+
const data = await apiCall('/api/market');
|
| 893 |
+
state.market = data;
|
| 894 |
+
renderMarketCards();
|
| 895 |
+
renderMarketTable();
|
| 896 |
+
updateMarketChart();
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
function renderMarketCards() {
|
| 900 |
+
const stats = state.market.global || {};
|
| 901 |
+
const container = document.getElementById('marketGlobalStats');
|
| 902 |
+
container.innerHTML = `
|
| 903 |
+
<div><strong>Total Market Cap:</strong> $${formatNumber(stats.total_market_cap)}</div>
|
| 904 |
+
<div><strong>Total Volume:</strong> $${formatNumber(stats.total_volume)}</div>
|
| 905 |
+
<div><strong>BTC Dominance:</strong> ${stats.btc_dominance ? stats.btc_dominance.toFixed(2) + '%' : '--'}</div>
|
| 906 |
+
<div><strong>ETH Dominance:</strong> ${stats.eth_dominance ? stats.eth_dominance.toFixed(2) + '%' : '--'}</div>
|
| 907 |
+
<div><strong>Active Cryptos:</strong> ${stats.active_cryptocurrencies || '--'}</div>
|
| 908 |
+
<div><strong>Markets:</strong> ${stats.markets || '--'}</div>
|
| 909 |
+
`;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
function renderMarketTable() {
|
| 913 |
+
const tbody = document.getElementById('marketTableBody');
|
| 914 |
+
const coins = state.market.cryptocurrencies || [];
|
| 915 |
+
if (!coins.length) {
|
| 916 |
+
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:40px; color:var(--muted);">Market data unavailable</td></tr>';
|
| 917 |
+
return;
|
| 918 |
+
}
|
| 919 |
+
tbody.innerHTML = coins.slice(0, 12).map(coin => `
|
| 920 |
+
<tr>
|
| 921 |
+
<td>${coin.rank || coin.market_cap_rank || '-'}</td>
|
| 922 |
+
<td>${coin.name} <span style="color:var(--muted);">${coin.symbol}</span></td>
|
| 923 |
+
<td>$${formatNumber(coin.price)}</td>
|
| 924 |
+
<td style="color:${coin.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${coin.change_24h ? coin.change_24h.toFixed(2) : '0'}%</td>
|
| 925 |
+
<td>$${formatNumber(coin.market_cap)}</td>
|
| 926 |
+
</tr>
|
| 927 |
+
`).join('');
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
function updateMarketChart() {
|
| 931 |
+
if (!state.charts.market) return;
|
| 932 |
+
const coins = state.market.cryptocurrencies || [];
|
| 933 |
+
const top = coins.slice(0, 5);
|
| 934 |
+
state.charts.market.data.labels = top.map(c => c.name);
|
| 935 |
+
state.charts.market.data.datasets[0].data = top.map(c => c.market_cap ? (c.market_cap / 1e9).toFixed(2) : 0);
|
| 936 |
+
state.charts.market.update();
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
async function loadTrending() {
|
| 940 |
+
const data = await apiCall('/api/trending');
|
| 941 |
+
state.trending = data.trending || [];
|
| 942 |
+
const list = document.getElementById('trendingList');
|
| 943 |
+
if (!state.trending.length) {
|
| 944 |
+
list.innerHTML = '<div class="list-item" style="color:var(--muted);">No trending assets</div>';
|
| 945 |
+
return;
|
| 946 |
+
}
|
| 947 |
+
list.innerHTML = state.trending.map(item => `
|
| 948 |
+
<div class="list-item">
|
| 949 |
+
<div style="font-weight:600;">${item.name} (${item.symbol})</div>
|
| 950 |
+
<div style="font-size:12px;color:var(--muted);">Rank: ${item.rank || '—'}</div>
|
| 951 |
+
</div>`).join('');
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
async function loadSentimentData() {
|
| 955 |
+
const data = await apiCall('/api/sentiment');
|
| 956 |
+
state.sentiment = data.fear_greed_index;
|
| 957 |
+
renderSentiment();
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
function renderSentiment() {
|
| 961 |
+
const container = document.getElementById('sentimentCard');
|
| 962 |
+
if (!state.sentiment) {
|
| 963 |
+
container.textContent = 'No sentiment data';
|
| 964 |
+
return;
|
| 965 |
+
}
|
| 966 |
+
const timestamp = Number(state.sentiment.timestamp);
|
| 967 |
+
container.innerHTML = `
|
| 968 |
+
<div style="font-size:32px; font-weight:700;">${state.sentiment.value}</div>
|
| 969 |
+
<div style="font-size:14px; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">${state.sentiment.classification}</div>
|
| 970 |
+
<div style="font-size:12px; color:var(--muted);">Updated: ${timestamp ? new Date(timestamp * 1000).toLocaleString() : '--'}</div>
|
| 971 |
+
`;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
async function loadDefi() {
|
| 975 |
+
const data = await apiCall('/api/defi');
|
| 976 |
+
state.defi = data.protocols || [];
|
| 977 |
+
const tbody = document.getElementById('defiTableBody');
|
| 978 |
+
if (!state.defi.length) {
|
| 979 |
+
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No DeFi data</td></tr>';
|
| 980 |
+
return;
|
| 981 |
+
}
|
| 982 |
+
tbody.innerHTML = state.defi.slice(0, 10).map(proto => `
|
| 983 |
+
<tr>
|
| 984 |
+
<td>${proto.name}</td>
|
| 985 |
+
<td>$${formatNumber(proto.tvl)}</td>
|
| 986 |
+
<td style="color:${proto.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${proto.change_24h ? proto.change_24h.toFixed(2) : 0}%</td>
|
| 987 |
+
<td>${proto.chain || '—'}</td>
|
| 988 |
+
</tr>`).join('');
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
async function loadNews() {
|
| 992 |
+
const data = await apiCall('/api/news');
|
| 993 |
+
state.news = data.articles || [];
|
| 994 |
+
const list = document.getElementById('newsList');
|
| 995 |
+
if (!state.news.length) {
|
| 996 |
+
list.innerHTML = '<div class="list-item" style="color:var(--muted);">No news available</div>';
|
| 997 |
+
return;
|
| 998 |
+
}
|
| 999 |
+
list.innerHTML = state.news.slice(0, 12).map(article => `
|
| 1000 |
+
<div class="news-item list-item">
|
| 1001 |
+
<h4>${article.title}</h4>
|
| 1002 |
+
<div class="news-meta">${article.source || 'Unknown'} • ${article.published_at ? new Date(article.published_at).toLocaleString() : ''}</div>
|
| 1003 |
+
<p style="margin:6px 0;">${article.description || ''}</p>
|
| 1004 |
+
<a href="${article.link}" target="_blank" style="font-size:12px; color:var(--primary);">Read article →</a>
|
| 1005 |
+
</div>`).join('');
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
async function loadErrorSummary() {
|
| 1009 |
+
try {
|
| 1010 |
+
const [summary, diagnostics] = await Promise.all([
|
| 1011 |
+
apiCall('/api/logs/summary'),
|
| 1012 |
+
apiCall('/api/diagnostics/errors')
|
| 1013 |
+
]);
|
| 1014 |
+
state.errorSummary = summary;
|
| 1015 |
+
state.diagnostics = diagnostics;
|
| 1016 |
+
renderErrorSummary();
|
| 1017 |
+
} catch (err) {
|
| 1018 |
+
console.error(err);
|
| 1019 |
+
document.getElementById('errorSummaryCard').textContent = 'Failed to load diagnostics';
|
| 1020 |
+
}
|
| 1021 |
+
}
|
| 1022 |
+
|
| 1023 |
+
function renderErrorSummary() {
|
| 1024 |
+
const summary = state.errorSummary;
|
| 1025 |
+
const card = document.getElementById('errorSummaryCard');
|
| 1026 |
+
if (!summary) {
|
| 1027 |
+
card.textContent = 'No diagnostics available';
|
| 1028 |
+
return;
|
| 1029 |
+
}
|
| 1030 |
+
card.innerHTML = `
|
| 1031 |
+
<div><strong>Total Logs:</strong> ${summary.total}</div>
|
| 1032 |
+
<div><strong>Last Error:</strong> ${summary.last_error ? summary.last_error.provider + ' @ ' + summary.last_error.timestamp : 'None'}</div>
|
| 1033 |
+
<div><strong>Top Offenders:</strong> ${Object.keys(summary.by_provider || {}).slice(0,3).join(', ') || '—'}</div>
|
| 1034 |
+
`;
|
| 1035 |
+
const diag = state.diagnostics || { recent: [] };
|
| 1036 |
+
const list = document.getElementById('diagnosticsList');
|
| 1037 |
+
list.innerHTML = diag.recent.slice(0,5).map(item => `
|
| 1038 |
+
<div class="list-item">
|
| 1039 |
+
<div style="font-weight:600;">${item.provider}</div>
|
| 1040 |
+
<div style="font-size:12px; color:var(--muted);">${item.timestamp}</div>
|
| 1041 |
+
<div style="font-size:13px; color:${item.status === 'offline' ? '#dc2626' : '#d97706'};">${item.message || 'No message'}</div>
|
| 1042 |
+
</div>`).join('');
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
async function loadResourceSearch() {
|
| 1046 |
+
const query = document.getElementById('resourceSearch')?.value || '';
|
| 1047 |
+
const source = document.getElementById('resourceFilter').value;
|
| 1048 |
+
const data = await apiCall(`/api/resources/search?q=${encodeURIComponent(query)}&source=${source}`);
|
| 1049 |
+
state.resources = data.results;
|
| 1050 |
+
document.getElementById('resourceCountProviders').textContent = `(${data.counts.providers})`;
|
| 1051 |
+
document.getElementById('resourceCountModels').textContent = `(${data.counts.models})`;
|
| 1052 |
+
document.getElementById('resourceCountDatasets').textContent = `(${data.counts.datasets})`;
|
| 1053 |
+
renderResourceResults();
|
| 1054 |
+
}
|
| 1055 |
+
|
| 1056 |
+
function renderResourceResults() {
|
| 1057 |
+
const providersContainer = document.getElementById('resourceResultsProviders');
|
| 1058 |
+
const modelsContainer = document.getElementById('resourceResultsModels');
|
| 1059 |
+
const datasetsContainer = document.getElementById('resourceResultsDatasets');
|
| 1060 |
+
|
| 1061 |
+
providersContainer.innerHTML = state.resources.providers.slice(0,6).map(p => `
|
| 1062 |
+
<div class="resource-item">
|
| 1063 |
+
<strong>${p.name}</strong>
|
| 1064 |
+
<div style="font-size:12px;color:var(--muted);">${p.category}</div>
|
| 1065 |
+
<div style="font-size:12px;">Status: ${p.status}</div>
|
| 1066 |
+
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1067 |
+
|
| 1068 |
+
modelsContainer.innerHTML = state.resources.models.slice(0,6).map(m => `
|
| 1069 |
+
<div class="resource-item">
|
| 1070 |
+
<strong>${m.id}</strong>
|
| 1071 |
+
<div style="font-size:12px;color:var(--muted);">${m.description || 'No description'}</div>
|
| 1072 |
+
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1073 |
+
|
| 1074 |
+
datasetsContainer.innerHTML = state.resources.datasets.slice(0,6).map(d => `
|
| 1075 |
+
<div class="resource-item">
|
| 1076 |
+
<strong>${d.id}</strong>
|
| 1077 |
+
<div style="font-size:12px;color:var(--muted);">${d.description || 'No description'}</div>
|
| 1078 |
+
</div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
async function handleExport(type) {
|
| 1082 |
+
try {
|
| 1083 |
+
const res = await apiCall(`/api/v2/export/${type}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
| 1084 |
+
state.exports.unshift({ type, url: res.download_url, timestamp: res.timestamp });
|
| 1085 |
+
renderExportHistory();
|
| 1086 |
+
showToast(`${type.toUpperCase()} export ready`, 'success');
|
| 1087 |
+
} catch (err) {
|
| 1088 |
+
console.error(err);
|
| 1089 |
+
showToast('Export failed', 'error');
|
| 1090 |
+
}
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
async function handleBackup() {
|
| 1094 |
+
try {
|
| 1095 |
+
const res = await apiCall('/api/v2/backup', { method: 'POST' });
|
| 1096 |
+
state.exports.unshift({ type: 'backup', url: res.download_url, timestamp: res.timestamp });
|
| 1097 |
+
renderExportHistory();
|
| 1098 |
+
showToast('Backup created', 'success');
|
| 1099 |
+
} catch (err) {
|
| 1100 |
+
console.error(err);
|
| 1101 |
+
showToast('Backup failed', 'error');
|
| 1102 |
+
}
|
| 1103 |
+
}
|
| 1104 |
+
|
| 1105 |
+
function renderExportHistory() {
|
| 1106 |
+
const container = document.getElementById('exportHistory');
|
| 1107 |
+
if (!state.exports.length) {
|
| 1108 |
+
container.innerHTML = '<div style="color:var(--muted); font-size:13px;">No exports yet</div>';
|
| 1109 |
+
return;
|
| 1110 |
+
}
|
| 1111 |
+
container.innerHTML = state.exports.slice(0,5).map(entry => `
|
| 1112 |
+
<div class="list-item" style="border-bottom:1px solid var(--border);">
|
| 1113 |
+
<div style="font-weight:600;">${entry.type.toUpperCase()}</div>
|
| 1114 |
+
<div style="font-size:12px; color:var(--muted);">${new Date(entry.timestamp).toLocaleString()}</div>
|
| 1115 |
+
<a href="${entry.url}" style="font-size:12px; color:var(--primary);" target="_blank">Download</a>
|
| 1116 |
+
</div>`).join('');
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
async function handleImportSingle(event) {
|
| 1120 |
+
event.preventDefault();
|
| 1121 |
+
const form = event.target;
|
| 1122 |
+
const payload = Object.fromEntries(new FormData(form).entries());
|
| 1123 |
+
payload.requires_key = form.elements['requires_key'].checked;
|
| 1124 |
+
payload.timeout_ms = Number(payload.timeout_ms) || 10000;
|
| 1125 |
+
try {
|
| 1126 |
+
await apiCall('/api/providers', {
|
| 1127 |
+
method: 'POST',
|
| 1128 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1129 |
+
body: JSON.stringify(payload)
|
| 1130 |
+
});
|
| 1131 |
+
showToast('Provider imported', 'success');
|
| 1132 |
+
form.reset();
|
| 1133 |
+
loadProviders();
|
| 1134 |
+
} catch (err) {
|
| 1135 |
+
console.error(err);
|
| 1136 |
+
showToast('Import failed', 'error');
|
| 1137 |
+
}
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
async function handleImportBulk() {
|
| 1141 |
+
const textarea = document.getElementById('bulkImportTextarea');
|
| 1142 |
+
if (!textarea.value.trim()) {
|
| 1143 |
+
showToast('Paste provider JSON first', 'error');
|
| 1144 |
+
return;
|
| 1145 |
+
}
|
| 1146 |
+
try {
|
| 1147 |
+
const providers = JSON.parse(textarea.value);
|
| 1148 |
+
await apiCall('/api/v2/import/providers', {
|
| 1149 |
+
method: 'POST',
|
| 1150 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1151 |
+
body: JSON.stringify({ providers })
|
| 1152 |
+
});
|
| 1153 |
+
showToast('Bulk import complete', 'success');
|
| 1154 |
+
textarea.value = '';
|
| 1155 |
+
loadProviders();
|
| 1156 |
+
} catch (err) {
|
| 1157 |
+
console.error(err);
|
| 1158 |
+
showToast('Bulk import failed', 'error');
|
| 1159 |
+
}
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
function switchTab(event, tabName) {
|
| 1163 |
+
document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
|
| 1164 |
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
| 1165 |
+
event.currentTarget.classList.add('active');
|
| 1166 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 1167 |
+
state.currentTab = tabName;
|
| 1168 |
+
if (tabName === 'market') loadMarket();
|
| 1169 |
+
if (tabName === 'sentiment') { loadSentimentData(); loadDefi(); }
|
| 1170 |
+
if (tabName === 'news') loadNews();
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
function startAutoRefresh() {
|
| 1174 |
+
setInterval(() => {
|
| 1175 |
+
if (state.wsConnected) {
|
| 1176 |
+
refreshAll();
|
| 1177 |
+
}
|
| 1178 |
+
}, config.autoRefreshInterval);
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
function refreshAll() {
|
| 1182 |
+
loadProviders();
|
| 1183 |
+
loadMarket();
|
| 1184 |
+
loadTrending();
|
| 1185 |
+
loadSentimentData();
|
| 1186 |
+
loadDefi();
|
| 1187 |
+
loadErrorSummary();
|
| 1188 |
+
loadNews();
|
| 1189 |
+
loadResourceSearch();
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
function formatNumber(value) {
|
| 1193 |
+
if (!value && value !== 0) return '--';
|
| 1194 |
+
if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
|
| 1195 |
+
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
|
| 1196 |
+
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
|
| 1197 |
+
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K';
|
| 1198 |
+
return Number(value).toFixed(2);
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
function setupResourceSearch() {
|
| 1202 |
+
const input = document.getElementById('resourceSearch');
|
| 1203 |
+
input.addEventListener('input', () => {
|
| 1204 |
+
clearTimeout(state.resourceSearchTimeout);
|
| 1205 |
+
state.resourceSearchTimeout = setTimeout(loadResourceSearch, 400);
|
| 1206 |
+
});
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
initializeWebSocket();
|
| 1210 |
+
setupResourceSearch();
|
| 1211 |
+
loadInitialData();
|
| 1212 |
+
startAutoRefresh();
|
| 1213 |
+
</script>
|
| 1214 |
+
</body>
|
| 1215 |
+
</html>
|
| 1216 |
+
|
package.json
CHANGED
|
@@ -10,12 +10,7 @@
|
|
| 10 |
"dashboard": "python3 -m http.server 8080",
|
| 11 |
"full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
|
| 12 |
"test:free-resources": "node free_resources_selftest.mjs",
|
| 13 |
-
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
|
| 14 |
-
"test:theme": "node tests/verify_theme.js",
|
| 15 |
-
"test:api-client": "node tests/test_apiClient.test.js"
|
| 16 |
-
},
|
| 17 |
-
"devDependencies": {
|
| 18 |
-
"fast-check": "^3.15.0"
|
| 19 |
},
|
| 20 |
"keywords": [
|
| 21 |
"cryptocurrency",
|
|
|
|
| 10 |
"dashboard": "python3 -m http.server 8080",
|
| 11 |
"full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
|
| 12 |
"test:free-resources": "node free_resources_selftest.mjs",
|
| 13 |
+
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
},
|
| 15 |
"keywords": [
|
| 16 |
"cryptocurrency",
|
static/css/design-system.css
CHANGED
|
@@ -148,7 +148,7 @@
|
|
| 148 |
|
| 149 |
:root {
|
| 150 |
/* ━━━ FONT FAMILIES ━━━ */
|
| 151 |
-
--font-main: "Inter", "
|
| 152 |
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, monospace;
|
| 153 |
|
| 154 |
/* ━━━ FONT SIZES ━━━ */
|
|
|
|
| 148 |
|
| 149 |
:root {
|
| 150 |
/* ━━━ FONT FAMILIES ━━━ */
|
| 151 |
+
--font-main: "Inter", "Rubik", "Vazirmatn", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 152 |
--font-mono: "JetBrains Mono", "Fira Code", "SF Mono", Monaco, Consolas, monospace;
|
| 153 |
|
| 154 |
/* ━━━ FONT SIZES ━━━ */
|
static/css/design-tokens.css
CHANGED
|
@@ -1,129 +1,96 @@
|
|
| 1 |
/**
|
| 2 |
* ============================================
|
| 3 |
-
*
|
| 4 |
-
* Crypto
|
| 5 |
* ============================================
|
| 6 |
*
|
| 7 |
-
*
|
| 8 |
-
* - Color palette (dark
|
| 9 |
-
* -
|
| 10 |
-
* -
|
| 11 |
-
* - Spacing system (consistent rhythm)
|
| 12 |
* - Border radius tokens
|
| 13 |
-
* -
|
| 14 |
-
* - Blur
|
| 15 |
-
* -
|
| 16 |
-
* -
|
| 17 |
-
* - Layout constants
|
| 18 |
*/
|
| 19 |
|
| 20 |
:root {
|
| 21 |
-
/* ===== COLOR PALETTE
|
| 22 |
|
| 23 |
-
/*
|
| 24 |
-
--color-primary: #
|
| 25 |
-
--color-
|
| 26 |
-
--color-
|
| 27 |
-
--color-
|
| 28 |
-
|
| 29 |
-
/* Accent Colors */
|
| 30 |
-
--color-accent: #ec4899;
|
| 31 |
-
--color-accent-light: #f472b6;
|
| 32 |
-
--color-accent-dark: #db2777;
|
| 33 |
-
|
| 34 |
-
/* Semantic Colors */
|
| 35 |
-
--color-success: #10b981;
|
| 36 |
-
--color-success-light: #34d399;
|
| 37 |
-
--color-success-dark: #059669;
|
| 38 |
-
|
| 39 |
-
--color-warning: #f59e0b;
|
| 40 |
-
--color-warning-light: #fbbf24;
|
| 41 |
-
--color-warning-dark: #d97706;
|
| 42 |
-
|
| 43 |
-
--color-error: #ef4444;
|
| 44 |
-
--color-error-light: #f87171;
|
| 45 |
-
--color-error-dark: #dc2626;
|
| 46 |
-
|
| 47 |
-
--color-info: #3b82f6;
|
| 48 |
-
--color-info-light: #60a5fa;
|
| 49 |
-
--color-info-dark: #2563eb;
|
| 50 |
-
|
| 51 |
-
/* Extended Palette */
|
| 52 |
-
--color-purple: #8b5cf6;
|
| 53 |
-
--color-purple-light: #a78bfa;
|
| 54 |
-
--color-purple-dark: #7c3aed;
|
| 55 |
-
|
| 56 |
-
--color-cyan: #06b6d4;
|
| 57 |
-
--color-cyan-light: #22d3ee;
|
| 58 |
-
--color-cyan-dark: #0891b2;
|
| 59 |
-
|
| 60 |
-
--color-orange: #f97316;
|
| 61 |
-
--color-orange-light: #fb923c;
|
| 62 |
-
--color-orange-dark: #ea580c;
|
| 63 |
-
|
| 64 |
-
/* Background Colors - Dark Theme */
|
| 65 |
-
--bg-primary: #0f172a;
|
| 66 |
-
--bg-secondary: #1e293b;
|
| 67 |
-
--bg-tertiary: #334155;
|
| 68 |
-
--bg-elevated: #1e293b;
|
| 69 |
-
--bg-overlay: rgba(0, 0, 0, 0.75);
|
| 70 |
|
| 71 |
/* Glassmorphism Backgrounds */
|
| 72 |
-
--glass-bg: rgba(
|
| 73 |
-
--glass-bg-light: rgba(
|
| 74 |
-
--glass-
|
| 75 |
-
--glass-border: rgba(255, 255, 255, 0.1);
|
| 76 |
-
--glass-border-strong: rgba(255, 255, 255, 0.2);
|
| 77 |
|
| 78 |
/* Text Colors */
|
| 79 |
-
--text-primary: #
|
| 80 |
-
--text-secondary: #
|
| 81 |
-
--text-tertiary: #
|
| 82 |
-
--text-
|
| 83 |
-
--text-
|
| 84 |
-
--text-inverse: #0f172a;
|
| 85 |
|
| 86 |
-
/*
|
| 87 |
-
--
|
| 88 |
-
--
|
| 89 |
-
--
|
| 90 |
-
--border-focus: var(--color-primary);
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
--
|
| 96 |
-
--
|
| 97 |
-
--gradient-success: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
| 98 |
-
--gradient-warning: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
| 99 |
-
--gradient-error: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
| 100 |
|
| 101 |
-
|
| 102 |
-
--
|
| 103 |
-
--
|
| 104 |
|
| 105 |
-
|
| 106 |
-
--
|
| 107 |
-
--
|
| 108 |
|
| 109 |
-
|
| 110 |
-
--
|
| 111 |
-
--
|
| 112 |
-
--gradient-radial-pink: radial-gradient(circle at 50% 50%, rgba(236,72,153,0.1) 0%, transparent 40%);
|
| 113 |
-
--gradient-radial-green: radial-gradient(circle at 60% 40%, rgba(16,185,129,0.1) 0%, transparent 40%);
|
| 114 |
|
| 115 |
-
|
| 116 |
-
--
|
| 117 |
-
--
|
| 118 |
-
--gradient-ocean: linear-gradient(135deg, #2e3192 0%, #1bffff 100%);
|
| 119 |
|
| 120 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
/*
|
| 123 |
-
--
|
| 124 |
-
--
|
| 125 |
-
--
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
/* Font Sizes */
|
| 129 |
--font-size-xs: 0.75rem; /* 12px */
|
|
@@ -135,7 +102,6 @@
|
|
| 135 |
--font-size-2xl: 1.875rem; /* 30px */
|
| 136 |
--font-size-3xl: 2.25rem; /* 36px */
|
| 137 |
--font-size-4xl: 3rem; /* 48px */
|
| 138 |
-
--font-size-5xl: 3.75rem; /* 60px */
|
| 139 |
|
| 140 |
/* Font Weights */
|
| 141 |
--font-weight-light: 300;
|
|
@@ -148,125 +114,74 @@
|
|
| 148 |
|
| 149 |
/* Line Heights */
|
| 150 |
--line-height-tight: 1.25;
|
| 151 |
-
--line-height-snug: 1.375;
|
| 152 |
--line-height-normal: 1.5;
|
| 153 |
-
--line-height-relaxed: 1.
|
| 154 |
-
--line-height-loose:
|
| 155 |
-
--line-height-loose-2: 2;
|
| 156 |
-
|
| 157 |
-
/* Letter Spacing */
|
| 158 |
-
--letter-spacing-tighter: -0.05em;
|
| 159 |
-
--letter-spacing-tight: -0.025em;
|
| 160 |
-
--letter-spacing-normal: 0;
|
| 161 |
-
--letter-spacing-wide: 0.025em;
|
| 162 |
-
--letter-spacing-wider: 0.05em;
|
| 163 |
-
--letter-spacing-widest: 0.1em;
|
| 164 |
|
| 165 |
/* ===== SPACING SCALE ===== */
|
| 166 |
-
--
|
| 167 |
-
--
|
| 168 |
-
--
|
| 169 |
-
--
|
| 170 |
-
--
|
| 171 |
-
--
|
| 172 |
-
--
|
| 173 |
-
--
|
| 174 |
-
--
|
| 175 |
-
--
|
| 176 |
-
--
|
| 177 |
-
--
|
| 178 |
-
--space-20: 5rem; /* 80px */
|
| 179 |
-
--space-24: 6rem; /* 96px */
|
| 180 |
-
--space-32: 8rem; /* 128px */
|
| 181 |
|
| 182 |
/* Semantic Spacing */
|
| 183 |
-
--spacing-xs: var(--
|
| 184 |
-
--spacing-sm: var(--
|
| 185 |
-
--spacing-md: var(--
|
| 186 |
-
--spacing-lg: var(--
|
| 187 |
-
--spacing-xl: var(--
|
| 188 |
-
--spacing-2xl: var(--
|
| 189 |
-
--spacing-3xl: var(--space-16);
|
| 190 |
|
| 191 |
/* ===== BORDER RADIUS ===== */
|
| 192 |
--radius-none: 0;
|
| 193 |
-
--radius-
|
| 194 |
-
--radius-sm: 0.375rem; /* 6px */
|
| 195 |
--radius-base: 0.5rem; /* 8px */
|
| 196 |
--radius-md: 0.75rem; /* 12px */
|
| 197 |
--radius-lg: 1rem; /* 16px */
|
| 198 |
-
--radius-xl: 1.
|
| 199 |
-
--radius-2xl:
|
| 200 |
-
--radius-3xl:
|
| 201 |
--radius-full: 9999px;
|
| 202 |
|
| 203 |
-
/* =====
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
--shadow-
|
| 207 |
-
--shadow-
|
| 208 |
-
--shadow-
|
| 209 |
-
--shadow-
|
| 210 |
-
--shadow-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
--shadow-
|
| 215 |
-
--shadow-
|
| 216 |
-
--shadow-
|
| 217 |
-
--shadow-glow-warning: 0 0 20px rgba(245,158,11,0.3);
|
| 218 |
-
--shadow-glow-error: 0 0 20px rgba(239,68,68,0.3);
|
| 219 |
-
|
| 220 |
-
/* Multi-layered Colored Shadows */
|
| 221 |
-
--shadow-blue: 0 10px 30px -5px rgba(59, 130, 246, 0.4), 0 0 15px rgba(59, 130, 246, 0.2);
|
| 222 |
-
--shadow-purple: 0 10px 30px -5px rgba(139, 92, 246, 0.4), 0 0 15px rgba(139, 92, 246, 0.2);
|
| 223 |
-
--shadow-pink: 0 10px 30px -5px rgba(236, 72, 153, 0.4), 0 0 15px rgba(236, 72, 153, 0.2);
|
| 224 |
-
--shadow-green: 0 10px 30px -5px rgba(16, 185, 129, 0.4), 0 0 15px rgba(16, 185, 129, 0.2);
|
| 225 |
-
--shadow-cyan: 0 10px 30px -5px rgba(6, 182, 212, 0.4), 0 0 15px rgba(6, 182, 212, 0.2);
|
| 226 |
|
| 227 |
/* Inner Shadows */
|
| 228 |
-
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.
|
| 229 |
-
--shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.
|
| 230 |
|
| 231 |
-
/* ===== BLUR
|
| 232 |
--blur-none: 0;
|
| 233 |
-
--blur-xs: 2px;
|
| 234 |
--blur-sm: 4px;
|
| 235 |
--blur-base: 8px;
|
| 236 |
--blur-md: 12px;
|
| 237 |
--blur-lg: 16px;
|
| 238 |
-
--blur-xl:
|
| 239 |
--blur-2xl: 40px;
|
| 240 |
--blur-3xl: 64px;
|
| 241 |
|
| 242 |
-
/* =====
|
| 243 |
-
|
| 244 |
-
/* Duration */
|
| 245 |
-
--transition-instant: 0ms;
|
| 246 |
-
--transition-fast: 150ms;
|
| 247 |
-
--transition-base: 250ms;
|
| 248 |
-
--transition-slow: 350ms;
|
| 249 |
-
--transition-slower: 500ms;
|
| 250 |
-
--transition-slowest: 700ms;
|
| 251 |
-
|
| 252 |
-
/* Easing Functions */
|
| 253 |
-
--ease-linear: linear;
|
| 254 |
-
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
| 255 |
-
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
| 256 |
-
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
| 257 |
-
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
| 258 |
-
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 259 |
-
--ease-smooth: cubic-bezier(0.25, 0.1, 0.25, 1);
|
| 260 |
-
|
| 261 |
-
/* Combined Transitions */
|
| 262 |
-
--transition-all-fast: all var(--transition-fast) var(--ease-out);
|
| 263 |
-
--transition-all-base: all var(--transition-base) var(--ease-in-out);
|
| 264 |
-
--transition-all-slow: all var(--transition-slow) var(--ease-in-out);
|
| 265 |
-
--transition-transform: transform var(--transition-base) var(--ease-out);
|
| 266 |
-
--transition-opacity: opacity var(--transition-base) var(--ease-out);
|
| 267 |
-
--transition-colors: color var(--transition-base) var(--ease-out), background-color var(--transition-base) var(--ease-out), border-color var(--transition-base) var(--ease-out);
|
| 268 |
-
|
| 269 |
-
/* ===== Z-INDEX ELEVATION LEVELS ===== */
|
| 270 |
--z-base: 0;
|
| 271 |
--z-dropdown: 1000;
|
| 272 |
--z-sticky: 1020;
|
|
@@ -276,13 +191,27 @@
|
|
| 276 |
--z-popover: 1060;
|
| 277 |
--z-tooltip: 1070;
|
| 278 |
--z-notification: 1080;
|
| 279 |
-
--z-max: 9999;
|
| 280 |
|
| 281 |
-
/* =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
--header-height: 72px;
|
| 283 |
--sidebar-width: 280px;
|
| 284 |
--sidebar-collapsed-width: 80px;
|
| 285 |
--mobile-nav-height: 64px;
|
|
|
|
| 286 |
--container-max-width: 1920px;
|
| 287 |
--content-max-width: 1440px;
|
| 288 |
|
|
@@ -294,80 +223,52 @@
|
|
| 294 |
--breakpoint-xl: 1024px;
|
| 295 |
--breakpoint-2xl: 1280px;
|
| 296 |
--breakpoint-3xl: 1440px;
|
| 297 |
-
--breakpoint-4xl: 1920px;
|
| 298 |
}
|
| 299 |
|
| 300 |
-
/* ===== LIGHT
|
| 301 |
[data-theme="light"] {
|
| 302 |
-
|
| 303 |
-
--bg-
|
| 304 |
-
--bg-
|
| 305 |
-
--bg-
|
| 306 |
-
--bg-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
--glass-
|
| 311 |
-
|
| 312 |
-
--
|
| 313 |
-
--
|
| 314 |
-
--
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
--
|
| 319 |
-
--
|
| 320 |
-
|
| 321 |
-
--
|
| 322 |
-
--
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
--
|
| 326 |
-
--
|
| 327 |
-
--
|
| 328 |
-
|
| 329 |
-
/* Glass Gradients */
|
| 330 |
-
--gradient-glass: linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.6) 100%);
|
| 331 |
-
--gradient-glass-strong: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%);
|
| 332 |
-
|
| 333 |
-
/* Overlay Gradients */
|
| 334 |
-
--gradient-overlay: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.8) 100%);
|
| 335 |
-
|
| 336 |
-
/* Shadows - Lighter for Light Theme */
|
| 337 |
-
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
| 338 |
-
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
| 339 |
-
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 340 |
-
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.08);
|
| 341 |
-
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.12), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
|
| 342 |
-
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
| 343 |
-
|
| 344 |
-
/* Inner Shadows */
|
| 345 |
-
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
| 346 |
-
--shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
| 347 |
}
|
| 348 |
|
| 349 |
/* ===== UTILITY CLASSES ===== */
|
| 350 |
|
| 351 |
/* Glassmorphism Effects */
|
| 352 |
.glass-effect {
|
| 353 |
-
background: var(--glass-bg);
|
| 354 |
-
backdrop-filter: blur(var(--blur-
|
| 355 |
-
|
| 356 |
-
border: 1px solid var(--glass-border);
|
| 357 |
}
|
| 358 |
|
| 359 |
.glass-effect-light {
|
| 360 |
-
background: var(--glass-bg-light);
|
| 361 |
-
backdrop-filter: blur(var(--blur-
|
| 362 |
-
|
| 363 |
-
border: 1px solid var(--glass-border);
|
| 364 |
-
}
|
| 365 |
-
|
| 366 |
-
.glass-effect-strong {
|
| 367 |
-
background: var(--glass-bg-strong);
|
| 368 |
-
backdrop-filter: blur(var(--blur-xl));
|
| 369 |
-
-webkit-backdrop-filter: blur(var(--blur-xl));
|
| 370 |
-
border: 1px solid var(--glass-border-strong);
|
| 371 |
}
|
| 372 |
|
| 373 |
/* Gradient Backgrounds */
|
|
@@ -375,12 +276,8 @@
|
|
| 375 |
background: var(--gradient-primary);
|
| 376 |
}
|
| 377 |
|
| 378 |
-
.bg-gradient-
|
| 379 |
-
background: var(--gradient-
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
.bg-gradient-success {
|
| 383 |
-
background: var(--gradient-success);
|
| 384 |
}
|
| 385 |
|
| 386 |
/* Text Gradients */
|
|
@@ -391,13 +288,6 @@
|
|
| 391 |
-webkit-text-fill-color: transparent;
|
| 392 |
}
|
| 393 |
|
| 394 |
-
.text-gradient-accent {
|
| 395 |
-
background: var(--gradient-accent);
|
| 396 |
-
-webkit-background-clip: text;
|
| 397 |
-
background-clip: text;
|
| 398 |
-
-webkit-text-fill-color: transparent;
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
/* Shadow Utilities */
|
| 402 |
.shadow-glow-blue {
|
| 403 |
box-shadow: var(--shadow-blue);
|
|
@@ -417,25 +307,13 @@
|
|
| 417 |
|
| 418 |
/* Animation Utilities */
|
| 419 |
.transition-fast {
|
| 420 |
-
transition: var(--
|
| 421 |
}
|
| 422 |
|
| 423 |
.transition-base {
|
| 424 |
-
transition: var(--
|
| 425 |
}
|
| 426 |
|
| 427 |
.transition-slow {
|
| 428 |
-
transition: var(--
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
/* Accessibility: Respect reduced motion preference */
|
| 432 |
-
@media (prefers-reduced-motion: reduce) {
|
| 433 |
-
*,
|
| 434 |
-
*::before,
|
| 435 |
-
*::after {
|
| 436 |
-
animation-duration: 0.01ms !important;
|
| 437 |
-
animation-iteration-count: 1 !important;
|
| 438 |
-
transition-duration: 0.01ms !important;
|
| 439 |
-
scroll-behavior: auto !important;
|
| 440 |
-
}
|
| 441 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
* ============================================
|
| 3 |
+
* DESIGN TOKENS - Enterprise Edition
|
| 4 |
+
* Crypto Monitor Ultimate
|
| 5 |
* ============================================
|
| 6 |
*
|
| 7 |
+
* Complete design system with:
|
| 8 |
+
* - Color palette (light/dark)
|
| 9 |
+
* - Typography scale
|
| 10 |
+
* - Spacing system
|
|
|
|
| 11 |
* - Border radius tokens
|
| 12 |
+
* - Shadow system
|
| 13 |
+
* - Blur tokens
|
| 14 |
+
* - Elevation levels
|
| 15 |
+
* - Animation timings
|
|
|
|
| 16 |
*/
|
| 17 |
|
| 18 |
:root {
|
| 19 |
+
/* ===== COLOR PALETTE ===== */
|
| 20 |
|
| 21 |
+
/* Base Colors - Dark Mode */
|
| 22 |
+
--color-bg-primary: #0a0e1a;
|
| 23 |
+
--color-bg-secondary: #111827;
|
| 24 |
+
--color-bg-tertiary: #1f2937;
|
| 25 |
+
--color-bg-elevated: #1f2937;
|
| 26 |
+
--color-bg-overlay: rgba(0, 0, 0, 0.75);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
/* Glassmorphism Backgrounds */
|
| 29 |
+
--color-glass-bg: rgba(17, 24, 39, 0.7);
|
| 30 |
+
--color-glass-bg-light: rgba(31, 41, 55, 0.5);
|
| 31 |
+
--color-glass-border: rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
|
| 32 |
|
| 33 |
/* Text Colors */
|
| 34 |
+
--color-text-primary: #f9fafb;
|
| 35 |
+
--color-text-secondary: #9ca3af;
|
| 36 |
+
--color-text-tertiary: #6b7280;
|
| 37 |
+
--color-text-disabled: #4b5563;
|
| 38 |
+
--color-text-inverse: #0a0e1a;
|
|
|
|
| 39 |
|
| 40 |
+
/* Accent Colors - Neon Palette */
|
| 41 |
+
--color-accent-blue: #3b82f6;
|
| 42 |
+
--color-accent-blue-dark: #2563eb;
|
| 43 |
+
--color-accent-blue-light: #60a5fa;
|
|
|
|
| 44 |
|
| 45 |
+
--color-accent-purple: #8b5cf6;
|
| 46 |
+
--color-accent-purple-dark: #7c3aed;
|
| 47 |
+
--color-accent-purple-light: #a78bfa;
|
| 48 |
|
| 49 |
+
--color-accent-pink: #ec4899;
|
| 50 |
+
--color-accent-pink-dark: #db2777;
|
| 51 |
+
--color-accent-pink-light: #f472b6;
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
+
--color-accent-green: #10b981;
|
| 54 |
+
--color-accent-green-dark: #059669;
|
| 55 |
+
--color-accent-green-light: #34d399;
|
| 56 |
|
| 57 |
+
--color-accent-yellow: #f59e0b;
|
| 58 |
+
--color-accent-yellow-dark: #d97706;
|
| 59 |
+
--color-accent-yellow-light: #fbbf24;
|
| 60 |
|
| 61 |
+
--color-accent-red: #ef4444;
|
| 62 |
+
--color-accent-red-dark: #dc2626;
|
| 63 |
+
--color-accent-red-light: #f87171;
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
--color-accent-cyan: #06b6d4;
|
| 66 |
+
--color-accent-cyan-dark: #0891b2;
|
| 67 |
+
--color-accent-cyan-light: #22d3ee;
|
|
|
|
| 68 |
|
| 69 |
+
/* Semantic Colors */
|
| 70 |
+
--color-success: var(--color-accent-green);
|
| 71 |
+
--color-error: var(--color-accent-red);
|
| 72 |
+
--color-warning: var(--color-accent-yellow);
|
| 73 |
+
--color-info: var(--color-accent-blue);
|
| 74 |
|
| 75 |
+
/* Border Colors */
|
| 76 |
+
--color-border-primary: rgba(255, 255, 255, 0.1);
|
| 77 |
+
--color-border-secondary: rgba(255, 255, 255, 0.05);
|
| 78 |
+
--color-border-focus: var(--color-accent-blue);
|
| 79 |
+
|
| 80 |
+
/* ===== GRADIENTS ===== */
|
| 81 |
+
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%);
|
| 82 |
+
--gradient-secondary: linear-gradient(135deg, #10b981 0%, #06b6d4 100%);
|
| 83 |
+
--gradient-glass: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.4) 100%);
|
| 84 |
+
--gradient-overlay: linear-gradient(180deg, rgba(10, 14, 26, 0) 0%, rgba(10, 14, 26, 0.8) 100%);
|
| 85 |
+
|
| 86 |
+
/* Radial Gradients for Background */
|
| 87 |
+
--gradient-radial-blue: radial-gradient(circle at 20% 30%, rgba(59, 130, 246, 0.15) 0%, transparent 40%);
|
| 88 |
+
--gradient-radial-purple: radial-gradient(circle at 80% 70%, rgba(139, 92, 246, 0.15) 0%, transparent 40%);
|
| 89 |
+
--gradient-radial-green: radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.1) 0%, transparent 30%);
|
| 90 |
+
|
| 91 |
+
/* ===== TYPOGRAPHY ===== */
|
| 92 |
+
--font-family-primary: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 93 |
+
--font-family-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
| 94 |
|
| 95 |
/* Font Sizes */
|
| 96 |
--font-size-xs: 0.75rem; /* 12px */
|
|
|
|
| 102 |
--font-size-2xl: 1.875rem; /* 30px */
|
| 103 |
--font-size-3xl: 2.25rem; /* 36px */
|
| 104 |
--font-size-4xl: 3rem; /* 48px */
|
|
|
|
| 105 |
|
| 106 |
/* Font Weights */
|
| 107 |
--font-weight-light: 300;
|
|
|
|
| 114 |
|
| 115 |
/* Line Heights */
|
| 116 |
--line-height-tight: 1.25;
|
|
|
|
| 117 |
--line-height-normal: 1.5;
|
| 118 |
+
--line-height-relaxed: 1.75;
|
| 119 |
+
--line-height-loose: 2;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
/* ===== SPACING SCALE ===== */
|
| 122 |
+
--spacing-0: 0;
|
| 123 |
+
--spacing-1: 0.25rem; /* 4px */
|
| 124 |
+
--spacing-2: 0.5rem; /* 8px */
|
| 125 |
+
--spacing-3: 0.75rem; /* 12px */
|
| 126 |
+
--spacing-4: 1rem; /* 16px */
|
| 127 |
+
--spacing-5: 1.25rem; /* 20px */
|
| 128 |
+
--spacing-6: 1.5rem; /* 24px */
|
| 129 |
+
--spacing-8: 2rem; /* 32px */
|
| 130 |
+
--spacing-10: 2.5rem; /* 40px */
|
| 131 |
+
--spacing-12: 3rem; /* 48px */
|
| 132 |
+
--spacing-16: 4rem; /* 64px */
|
| 133 |
+
--spacing-20: 5rem; /* 80px */
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
/* Semantic Spacing */
|
| 136 |
+
--spacing-xs: var(--spacing-1);
|
| 137 |
+
--spacing-sm: var(--spacing-2);
|
| 138 |
+
--spacing-md: var(--spacing-4);
|
| 139 |
+
--spacing-lg: var(--spacing-6);
|
| 140 |
+
--spacing-xl: var(--spacing-8);
|
| 141 |
+
--spacing-2xl: var(--spacing-12);
|
|
|
|
| 142 |
|
| 143 |
/* ===== BORDER RADIUS ===== */
|
| 144 |
--radius-none: 0;
|
| 145 |
+
--radius-sm: 0.25rem; /* 4px */
|
|
|
|
| 146 |
--radius-base: 0.5rem; /* 8px */
|
| 147 |
--radius-md: 0.75rem; /* 12px */
|
| 148 |
--radius-lg: 1rem; /* 16px */
|
| 149 |
+
--radius-xl: 1.25rem; /* 20px */
|
| 150 |
+
--radius-2xl: 1.5rem; /* 24px */
|
| 151 |
+
--radius-3xl: 2rem; /* 32px */
|
| 152 |
--radius-full: 9999px;
|
| 153 |
|
| 154 |
+
/* ===== SHADOWS ===== */
|
| 155 |
+
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
| 156 |
+
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
| 157 |
+
--shadow-base: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 158 |
+
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
| 159 |
+
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
| 160 |
+
--shadow-xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
| 161 |
+
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
| 162 |
+
|
| 163 |
+
/* Colored Shadows */
|
| 164 |
+
--shadow-blue: 0 10px 30px -5px rgba(59, 130, 246, 0.3);
|
| 165 |
+
--shadow-purple: 0 10px 30px -5px rgba(139, 92, 246, 0.3);
|
| 166 |
+
--shadow-pink: 0 10px 30px -5px rgba(236, 72, 153, 0.3);
|
| 167 |
+
--shadow-green: 0 10px 30px -5px rgba(16, 185, 129, 0.3);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
/* Inner Shadows */
|
| 170 |
+
--shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
|
| 171 |
+
--shadow-inner-lg: inset 0 4px 8px 0 rgba(0, 0, 0, 0.1);
|
| 172 |
|
| 173 |
+
/* ===== BLUR TOKENS ===== */
|
| 174 |
--blur-none: 0;
|
|
|
|
| 175 |
--blur-sm: 4px;
|
| 176 |
--blur-base: 8px;
|
| 177 |
--blur-md: 12px;
|
| 178 |
--blur-lg: 16px;
|
| 179 |
+
--blur-xl: 20px;
|
| 180 |
--blur-2xl: 40px;
|
| 181 |
--blur-3xl: 64px;
|
| 182 |
|
| 183 |
+
/* ===== ELEVATION LEVELS ===== */
|
| 184 |
+
/* Use these for layering UI elements */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
--z-base: 0;
|
| 186 |
--z-dropdown: 1000;
|
| 187 |
--z-sticky: 1020;
|
|
|
|
| 191 |
--z-popover: 1060;
|
| 192 |
--z-tooltip: 1070;
|
| 193 |
--z-notification: 1080;
|
|
|
|
| 194 |
|
| 195 |
+
/* ===== ANIMATION TIMINGS ===== */
|
| 196 |
+
--duration-instant: 0ms;
|
| 197 |
+
--duration-fast: 150ms;
|
| 198 |
+
--duration-base: 250ms;
|
| 199 |
+
--duration-slow: 350ms;
|
| 200 |
+
--duration-slower: 500ms;
|
| 201 |
+
|
| 202 |
+
/* Easing Functions */
|
| 203 |
+
--ease-linear: linear;
|
| 204 |
+
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
| 205 |
+
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
| 206 |
+
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
| 207 |
+
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
| 208 |
+
|
| 209 |
+
/* ===== LAYOUT ===== */
|
| 210 |
--header-height: 72px;
|
| 211 |
--sidebar-width: 280px;
|
| 212 |
--sidebar-collapsed-width: 80px;
|
| 213 |
--mobile-nav-height: 64px;
|
| 214 |
+
|
| 215 |
--container-max-width: 1920px;
|
| 216 |
--content-max-width: 1440px;
|
| 217 |
|
|
|
|
| 223 |
--breakpoint-xl: 1024px;
|
| 224 |
--breakpoint-2xl: 1280px;
|
| 225 |
--breakpoint-3xl: 1440px;
|
|
|
|
| 226 |
}
|
| 227 |
|
| 228 |
+
/* ===== LIGHT MODE OVERRIDES ===== */
|
| 229 |
[data-theme="light"] {
|
| 230 |
+
--color-bg-primary: #ffffff;
|
| 231 |
+
--color-bg-secondary: #f9fafb;
|
| 232 |
+
--color-bg-tertiary: #f3f4f6;
|
| 233 |
+
--color-bg-elevated: #ffffff;
|
| 234 |
+
--color-bg-overlay: rgba(255, 255, 255, 0.9);
|
| 235 |
+
|
| 236 |
+
--color-glass-bg: rgba(255, 255, 255, 0.7);
|
| 237 |
+
--color-glass-bg-light: rgba(249, 250, 251, 0.5);
|
| 238 |
+
--color-glass-border: rgba(0, 0, 0, 0.1);
|
| 239 |
+
|
| 240 |
+
--color-text-primary: #111827;
|
| 241 |
+
--color-text-secondary: #6b7280;
|
| 242 |
+
--color-text-tertiary: #9ca3af;
|
| 243 |
+
--color-text-disabled: #d1d5db;
|
| 244 |
+
--color-text-inverse: #ffffff;
|
| 245 |
+
|
| 246 |
+
--color-border-primary: rgba(0, 0, 0, 0.1);
|
| 247 |
+
--color-border-secondary: rgba(0, 0, 0, 0.05);
|
| 248 |
+
|
| 249 |
+
--gradient-glass: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(249, 250, 251, 0.4) 100%);
|
| 250 |
+
--gradient-overlay: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 100%);
|
| 251 |
+
|
| 252 |
+
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.03);
|
| 253 |
+
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.08), 0 1px 2px 0 rgba(0, 0, 0, 0.04);
|
| 254 |
+
--shadow-base: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
|
| 255 |
+
--shadow-md: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.03);
|
| 256 |
+
--shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.02);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
/* ===== UTILITY CLASSES ===== */
|
| 260 |
|
| 261 |
/* Glassmorphism Effects */
|
| 262 |
.glass-effect {
|
| 263 |
+
background: var(--color-glass-bg);
|
| 264 |
+
backdrop-filter: blur(var(--blur-xl));
|
| 265 |
+
border: 1px solid var(--color-glass-border);
|
|
|
|
| 266 |
}
|
| 267 |
|
| 268 |
.glass-effect-light {
|
| 269 |
+
background: var(--color-glass-bg-light);
|
| 270 |
+
backdrop-filter: blur(var(--blur-lg));
|
| 271 |
+
border: 1px solid var(--color-glass-border);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
}
|
| 273 |
|
| 274 |
/* Gradient Backgrounds */
|
|
|
|
| 276 |
background: var(--gradient-primary);
|
| 277 |
}
|
| 278 |
|
| 279 |
+
.bg-gradient-secondary {
|
| 280 |
+
background: var(--gradient-secondary);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
}
|
| 282 |
|
| 283 |
/* Text Gradients */
|
|
|
|
| 288 |
-webkit-text-fill-color: transparent;
|
| 289 |
}
|
| 290 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
/* Shadow Utilities */
|
| 292 |
.shadow-glow-blue {
|
| 293 |
box-shadow: var(--shadow-blue);
|
|
|
|
| 307 |
|
| 308 |
/* Animation Utilities */
|
| 309 |
.transition-fast {
|
| 310 |
+
transition: all var(--duration-fast) var(--ease-out);
|
| 311 |
}
|
| 312 |
|
| 313 |
.transition-base {
|
| 314 |
+
transition: all var(--duration-base) var(--ease-in-out);
|
| 315 |
}
|
| 316 |
|
| 317 |
.transition-slow {
|
| 318 |
+
transition: all var(--duration-slow) var(--ease-in-out);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
}
|
static/css/pro-dashboard.css
CHANGED
|
@@ -1,34 +1,22 @@
|
|
| 1 |
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
| 2 |
|
| 3 |
:root {
|
| 4 |
-
--bg-gradient: radial-gradient(circle at top, #
|
| 5 |
-
--glass-bg: rgba(
|
| 6 |
-
--glass-border: rgba(255, 255, 255, 0.
|
| 7 |
-
--glass-highlight: rgba(255, 255, 255, 0.
|
| 8 |
--primary: #8f88ff;
|
| 9 |
--primary-strong: #6c63ff;
|
| 10 |
-
--primary-light: #b8b3ff;
|
| 11 |
-
--primary-glow: rgba(143, 136, 255, 0.4);
|
| 12 |
--secondary: #16d9fa;
|
| 13 |
-
--secondary-light: #5ee3f5;
|
| 14 |
-
--secondary-glow: rgba(22, 217, 250, 0.4);
|
| 15 |
--accent: #f472b6;
|
| 16 |
-
--success: #
|
| 17 |
-
--success-light: #86efac;
|
| 18 |
-
--success-glow: rgba(74, 222, 128, 0.5);
|
| 19 |
-
--success-dark: #22c55e;
|
| 20 |
--warning: #facc15;
|
| 21 |
-
--warning-light: #fde047;
|
| 22 |
--danger: #ef4444;
|
| 23 |
-
--danger-light: #f87171;
|
| 24 |
--info: #38bdf8;
|
| 25 |
-
--
|
| 26 |
-
--text-
|
| 27 |
-
--
|
| 28 |
-
--
|
| 29 |
-
--text-faint: rgba(226, 232, 240, 0.6);
|
| 30 |
-
--shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.7);
|
| 31 |
-
--shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.6);
|
| 32 |
--sidebar-width: 260px;
|
| 33 |
}
|
| 34 |
|
|
@@ -40,16 +28,9 @@ html, body {
|
|
| 40 |
margin: 0;
|
| 41 |
padding: 0;
|
| 42 |
min-height: 100vh;
|
| 43 |
-
font-family: '
|
| 44 |
-
font-weight: 500;
|
| 45 |
-
font-size: 15px;
|
| 46 |
-
line-height: 1.65;
|
| 47 |
-
letter-spacing: -0.015em;
|
| 48 |
background: var(--bg-gradient);
|
| 49 |
color: var(--text-primary);
|
| 50 |
-
-webkit-font-smoothing: antialiased;
|
| 51 |
-
-moz-osx-font-smoothing: grayscale;
|
| 52 |
-
text-rendering: optimizeLegibility;
|
| 53 |
}
|
| 54 |
|
| 55 |
body[data-theme='light'] {
|
|
@@ -68,137 +49,39 @@ body[data-theme='light'] {
|
|
| 68 |
|
| 69 |
.sidebar {
|
| 70 |
width: var(--sidebar-width);
|
| 71 |
-
padding:
|
| 72 |
-
background: linear-gradient(180deg,
|
| 73 |
-
#ffffff 0%,
|
| 74 |
-
rgba(219, 234, 254, 0.3) 20%,
|
| 75 |
-
rgba(221, 214, 254, 0.25) 40%,
|
| 76 |
-
rgba(251, 207, 232, 0.2) 60%,
|
| 77 |
-
rgba(221, 214, 254, 0.25) 80%,
|
| 78 |
-
rgba(251, 207, 232, 0.15) 90%,
|
| 79 |
-
#ffffff 100%);
|
| 80 |
backdrop-filter: blur(30px);
|
| 81 |
-
border-right: 1px solid
|
| 82 |
display: flex;
|
| 83 |
flex-direction: column;
|
| 84 |
-
gap:
|
| 85 |
position: sticky;
|
| 86 |
top: 0;
|
| 87 |
height: 100vh;
|
| 88 |
-
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.08), inset -1px 0 0 rgba(255, 255, 255, 0.5);
|
| 89 |
}
|
| 90 |
|
| 91 |
.brand {
|
| 92 |
-
display: flex;
|
| 93 |
-
align-items: center;
|
| 94 |
-
gap: 12px;
|
| 95 |
-
padding: 14px;
|
| 96 |
-
background: rgba(255, 255, 255, 0.03);
|
| 97 |
-
border-radius: 14px;
|
| 98 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 99 |
-
box-shadow:
|
| 100 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.05),
|
| 101 |
-
0 2px 8px rgba(0, 0, 0, 0.2);
|
| 102 |
-
position: relative;
|
| 103 |
-
overflow: hidden;
|
| 104 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
.brand::before {
|
| 108 |
-
content: '';
|
| 109 |
-
position: absolute;
|
| 110 |
-
inset: 0;
|
| 111 |
-
background: linear-gradient(135deg, rgba(143, 136, 255, 0.08), rgba(22, 217, 250, 0.05));
|
| 112 |
-
opacity: 0;
|
| 113 |
-
transition: opacity 0.4s ease;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.brand:hover {
|
| 117 |
-
background: rgba(255, 255, 255, 0.05);
|
| 118 |
-
border-color: rgba(143, 136, 255, 0.2);
|
| 119 |
-
box-shadow:
|
| 120 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.08),
|
| 121 |
-
0 4px 16px rgba(143, 136, 255, 0.15);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
.brand:hover::before {
|
| 125 |
-
opacity: 1;
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
.brand-icon {
|
| 129 |
-
display: flex;
|
| 130 |
-
align-items: center;
|
| 131 |
-
justify-content: center;
|
| 132 |
-
width: 40px;
|
| 133 |
-
height: 40px;
|
| 134 |
-
border-radius: 10px;
|
| 135 |
-
background: rgba(255, 255, 255, 0.04);
|
| 136 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 137 |
-
color: var(--text-secondary);
|
| 138 |
-
flex-shrink: 0;
|
| 139 |
-
box-shadow:
|
| 140 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.1),
|
| 141 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
| 142 |
-
0 2px 4px rgba(0, 0, 0, 0.2);
|
| 143 |
-
position: relative;
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
.brand-icon::before {
|
| 147 |
-
content: '';
|
| 148 |
-
position: absolute;
|
| 149 |
-
top: 2px;
|
| 150 |
-
left: 2px;
|
| 151 |
-
right: 2px;
|
| 152 |
-
height: 50%;
|
| 153 |
-
border-radius: 10px 10px 0 0;
|
| 154 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent);
|
| 155 |
-
pointer-events: none;
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
.brand-icon svg {
|
| 159 |
-
position: relative;
|
| 160 |
-
z-index: 1;
|
| 161 |
-
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
.brand-text {
|
| 165 |
display: flex;
|
| 166 |
flex-direction: column;
|
| 167 |
gap: 6px;
|
| 168 |
-
flex: 1;
|
| 169 |
-
min-width: 0;
|
| 170 |
}
|
| 171 |
|
| 172 |
.brand strong {
|
| 173 |
-
font-size:
|
| 174 |
-
|
| 175 |
-
letter-spacing: -0.01em;
|
| 176 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 177 |
-
color: var(--text-primary);
|
| 178 |
-
line-height: 1.3;
|
| 179 |
-
white-space: nowrap;
|
| 180 |
-
overflow: hidden;
|
| 181 |
-
text-overflow: ellipsis;
|
| 182 |
}
|
| 183 |
|
| 184 |
.env-pill {
|
| 185 |
display: inline-flex;
|
| 186 |
align-items: center;
|
| 187 |
-
gap:
|
| 188 |
-
background: rgba(
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
font-size: 0.65rem;
|
| 193 |
-
font-weight: 600;
|
| 194 |
text-transform: uppercase;
|
| 195 |
-
letter-spacing: 0.
|
| 196 |
-
color: rgba(143, 136, 255, 0.9);
|
| 197 |
-
font-family: 'Manrope', sans-serif;
|
| 198 |
-
white-space: nowrap;
|
| 199 |
-
overflow: hidden;
|
| 200 |
-
text-overflow: ellipsis;
|
| 201 |
-
max-width: 100%;
|
| 202 |
}
|
| 203 |
|
| 204 |
.nav {
|
|
@@ -209,199 +92,34 @@ body[data-theme='light'] {
|
|
| 209 |
|
| 210 |
.nav-button {
|
| 211 |
border: none;
|
| 212 |
-
border-radius:
|
| 213 |
padding: 12px 16px;
|
| 214 |
display: flex;
|
| 215 |
align-items: center;
|
| 216 |
gap: 12px;
|
| 217 |
background: transparent;
|
| 218 |
-
color:
|
| 219 |
-
font-weight:
|
| 220 |
-
font-family: 'Manrope', sans-serif;
|
| 221 |
-
font-size: 0.875rem;
|
| 222 |
cursor: pointer;
|
| 223 |
-
transition:
|
| 224 |
-
position: relative;
|
| 225 |
-
overflow: visible;
|
| 226 |
-
}
|
| 227 |
-
|
| 228 |
-
.nav-button svg {
|
| 229 |
-
color: #6366f1;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
.nav-button[data-nav="page-overview"] svg {
|
| 233 |
-
color: #3b82f6;
|
| 234 |
-
}
|
| 235 |
-
|
| 236 |
-
.nav-button[data-nav="page-market"] svg {
|
| 237 |
-
color: #8b5cf6;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.nav-button[data-nav="page-chart"] svg {
|
| 241 |
-
color: #ec4899;
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
.nav-button[data-nav="page-ai"] svg {
|
| 245 |
-
color: #10b981;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
.nav-button[data-nav="page-news"] svg {
|
| 249 |
-
color: #f59e0b;
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
.nav-button[data-nav="page-providers"] svg {
|
| 253 |
-
color: #06b6d4;
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
.nav-button[data-nav="page-api"] svg {
|
| 257 |
-
color: #6366f1;
|
| 258 |
-
}
|
| 259 |
-
|
| 260 |
-
.nav-button[data-nav="page-debug"] svg {
|
| 261 |
-
color: #ef4444;
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
.nav-button[data-nav="page-datasets"] svg {
|
| 265 |
-
color: #a855f7;
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
.nav-button[data-nav="page-settings"] svg {
|
| 269 |
-
color: #64748b;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
.nav-button::before {
|
| 273 |
-
content: '';
|
| 274 |
-
position: absolute;
|
| 275 |
-
left: 0;
|
| 276 |
-
top: 50%;
|
| 277 |
-
transform: translateY(-50%);
|
| 278 |
-
width: 2px;
|
| 279 |
-
height: 0;
|
| 280 |
-
background: var(--primary);
|
| 281 |
-
border-radius: 0 2px 2px 0;
|
| 282 |
-
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 283 |
-
opacity: 0;
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
.nav-button::after {
|
| 287 |
-
content: '';
|
| 288 |
-
position: absolute;
|
| 289 |
-
inset: 0;
|
| 290 |
-
background: rgba(143, 136, 255, 0.05);
|
| 291 |
-
border-radius: 10px;
|
| 292 |
-
opacity: 0;
|
| 293 |
-
transition: opacity 0.25s ease;
|
| 294 |
-
z-index: -1;
|
| 295 |
}
|
| 296 |
|
| 297 |
.nav-button svg {
|
| 298 |
-
width:
|
| 299 |
-
height:
|
| 300 |
fill: currentColor;
|
| 301 |
-
stroke: currentColor;
|
| 302 |
-
stroke-width: 2;
|
| 303 |
-
transition: all 0.25s ease;
|
| 304 |
-
flex-shrink: 0;
|
| 305 |
-
opacity: 1;
|
| 306 |
-
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
| 307 |
}
|
| 308 |
|
|
|
|
| 309 |
.nav-button:hover {
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
.nav-button:hover svg {
|
| 315 |
-
opacity: 1;
|
| 316 |
-
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15));
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
.nav-button:hover::before {
|
| 320 |
-
height: 50%;
|
| 321 |
-
opacity: 1;
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
.nav-button:hover::after {
|
| 325 |
-
opacity: 1;
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
.nav-button:hover svg {
|
| 329 |
-
opacity: 1;
|
| 330 |
-
color: var(--primary);
|
| 331 |
-
}
|
| 332 |
-
|
| 333 |
-
.nav-button.active {
|
| 334 |
-
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.12));
|
| 335 |
-
color: #0f172a;
|
| 336 |
-
box-shadow:
|
| 337 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.3),
|
| 338 |
-
0 2px 8px rgba(99, 102, 241, 0.2);
|
| 339 |
-
border: 1px solid rgba(99, 102, 241, 0.2);
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
.nav-button.active svg {
|
| 343 |
-
opacity: 1;
|
| 344 |
-
filter: drop-shadow(0 2px 4px rgba(99, 102, 241, 0.3));
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
.nav-button.active::before {
|
| 348 |
-
height: 60%;
|
| 349 |
-
opacity: 1;
|
| 350 |
-
box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
.nav-button.active::after {
|
| 354 |
-
opacity: 1;
|
| 355 |
-
background: rgba(143, 136, 255, 0.08);
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
.nav-button.active svg {
|
| 359 |
-
color: var(--primary);
|
| 360 |
-
opacity: 1;
|
| 361 |
}
|
| 362 |
|
| 363 |
.sidebar-footer {
|
| 364 |
margin-top: auto;
|
| 365 |
-
|
| 366 |
-
display: flex;
|
| 367 |
-
align-items: center;
|
| 368 |
-
justify-content: center;
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
.footer-badge {
|
| 372 |
-
display: inline-flex;
|
| 373 |
-
align-items: center;
|
| 374 |
-
gap: 8px;
|
| 375 |
-
padding: 10px 16px;
|
| 376 |
-
background: rgba(255, 255, 255, 0.03);
|
| 377 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 378 |
-
border-radius: 12px;
|
| 379 |
-
font-size: 0.75rem;
|
| 380 |
-
font-weight: 600;
|
| 381 |
color: var(--text-muted);
|
| 382 |
-
font-family: 'Manrope', sans-serif;
|
| 383 |
-
letter-spacing: 0.05em;
|
| 384 |
-
text-transform: uppercase;
|
| 385 |
-
transition: all 0.3s ease;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
.footer-badge svg {
|
| 389 |
-
width: 14px;
|
| 390 |
-
height: 14px;
|
| 391 |
-
opacity: 0.7;
|
| 392 |
-
transition: all 0.3s ease;
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
.footer-badge:hover {
|
| 396 |
-
background: rgba(255, 255, 255, 0.06);
|
| 397 |
-
border-color: rgba(143, 136, 255, 0.3);
|
| 398 |
-
color: var(--text-primary);
|
| 399 |
-
transform: translateY(-2px);
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
.footer-badge:hover svg {
|
| 403 |
-
opacity: 1;
|
| 404 |
-
color: var(--primary);
|
| 405 |
}
|
| 406 |
|
| 407 |
.main-area {
|
|
@@ -416,125 +134,19 @@ body[data-theme='light'] {
|
|
| 416 |
display: flex;
|
| 417 |
justify-content: space-between;
|
| 418 |
align-items: center;
|
| 419 |
-
padding: 24px
|
| 420 |
border-radius: 24px;
|
| 421 |
-
background:
|
| 422 |
-
border: 1px solid
|
| 423 |
-
box-shadow:
|
| 424 |
-
var(--shadow-soft),
|
| 425 |
-
0 0 0 1px rgba(255, 255, 255, 0.08) inset,
|
| 426 |
-
0 0 40px rgba(143, 136, 255, 0.15),
|
| 427 |
-
0 4px 16px rgba(0, 0, 0, 0.3);
|
| 428 |
backdrop-filter: blur(20px);
|
| 429 |
flex-wrap: wrap;
|
| 430 |
-
gap: 20px;
|
| 431 |
-
position: relative;
|
| 432 |
-
overflow: hidden;
|
| 433 |
-
}
|
| 434 |
-
|
| 435 |
-
.topbar::before {
|
| 436 |
-
content: '';
|
| 437 |
-
position: absolute;
|
| 438 |
-
top: 0;
|
| 439 |
-
left: 0;
|
| 440 |
-
right: 0;
|
| 441 |
-
height: 2px;
|
| 442 |
-
background: linear-gradient(90deg, transparent, rgba(143, 136, 255, 0.5), rgba(22, 217, 250, 0.5), transparent);
|
| 443 |
-
}
|
| 444 |
-
|
| 445 |
-
.topbar-content {
|
| 446 |
-
display: flex;
|
| 447 |
-
align-items: center;
|
| 448 |
gap: 16px;
|
| 449 |
-
flex: 1;
|
| 450 |
-
}
|
| 451 |
-
|
| 452 |
-
.topbar-icon {
|
| 453 |
-
display: flex;
|
| 454 |
-
align-items: center;
|
| 455 |
-
justify-content: center;
|
| 456 |
-
width: 64px;
|
| 457 |
-
height: 64px;
|
| 458 |
-
border-radius: 14px;
|
| 459 |
-
background: rgba(255, 255, 255, 0.04);
|
| 460 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 461 |
-
color: var(--text-secondary);
|
| 462 |
-
flex-shrink: 0;
|
| 463 |
-
box-shadow:
|
| 464 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.1),
|
| 465 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.2),
|
| 466 |
-
0 2px 4px rgba(0, 0, 0, 0.2);
|
| 467 |
-
position: relative;
|
| 468 |
-
}
|
| 469 |
-
|
| 470 |
-
.topbar-icon::before {
|
| 471 |
-
content: '';
|
| 472 |
-
position: absolute;
|
| 473 |
-
top: 2px;
|
| 474 |
-
left: 2px;
|
| 475 |
-
right: 2px;
|
| 476 |
-
height: 50%;
|
| 477 |
-
border-radius: 14px 14px 0 0;
|
| 478 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), transparent);
|
| 479 |
-
pointer-events: none;
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
.topbar-icon svg {
|
| 483 |
-
position: relative;
|
| 484 |
-
z-index: 1;
|
| 485 |
-
width: 36px;
|
| 486 |
-
height: 36px;
|
| 487 |
-
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
| 488 |
-
}
|
| 489 |
-
|
| 490 |
-
.topbar-text {
|
| 491 |
-
display: flex;
|
| 492 |
-
flex-direction: column;
|
| 493 |
-
gap: 6px;
|
| 494 |
}
|
| 495 |
|
| 496 |
.topbar h1 {
|
| 497 |
margin: 0;
|
| 498 |
-
font-size:
|
| 499 |
-
font-weight: 800;
|
| 500 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 501 |
-
letter-spacing: -0.03em;
|
| 502 |
-
line-height: 1.2;
|
| 503 |
-
display: flex;
|
| 504 |
-
align-items: baseline;
|
| 505 |
-
gap: 8px;
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
.title-gradient {
|
| 509 |
-
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 80%, #cbd5e1 100%);
|
| 510 |
-
-webkit-background-clip: text;
|
| 511 |
-
-webkit-text-fill-color: transparent;
|
| 512 |
-
background-clip: text;
|
| 513 |
-
text-shadow: 0 0 30px rgba(255, 255, 255, 0.3);
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
.title-accent {
|
| 517 |
-
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
| 518 |
-
-webkit-background-clip: text;
|
| 519 |
-
-webkit-text-fill-color: transparent;
|
| 520 |
-
background-clip: text;
|
| 521 |
-
font-size: 0.9em;
|
| 522 |
-
}
|
| 523 |
-
|
| 524 |
-
.topbar p.text-muted {
|
| 525 |
-
margin: 0;
|
| 526 |
-
font-size: 0.875rem;
|
| 527 |
-
color: var(--text-muted);
|
| 528 |
-
font-weight: 500;
|
| 529 |
-
font-family: 'Manrope', sans-serif;
|
| 530 |
-
display: flex;
|
| 531 |
-
align-items: center;
|
| 532 |
-
gap: 4px;
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
.topbar p.text-muted svg {
|
| 536 |
-
opacity: 0.7;
|
| 537 |
-
color: var(--primary);
|
| 538 |
}
|
| 539 |
|
| 540 |
.status-group {
|
|
@@ -546,247 +158,33 @@ body[data-theme='light'] {
|
|
| 546 |
.status-pill {
|
| 547 |
display: flex;
|
| 548 |
align-items: center;
|
| 549 |
-
gap:
|
| 550 |
-
padding:
|
| 551 |
-
border-radius:
|
| 552 |
-
background: rgba(255, 255, 255, 0.
|
| 553 |
-
border:
|
| 554 |
-
font-size: 0.
|
| 555 |
-
font-weight: 700;
|
| 556 |
text-transform: uppercase;
|
| 557 |
-
letter-spacing: 0.
|
| 558 |
-
font-family: 'Manrope', sans-serif;
|
| 559 |
-
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 560 |
-
position: relative;
|
| 561 |
-
overflow: visible;
|
| 562 |
-
box-shadow:
|
| 563 |
-
0 2px 8px rgba(0, 0, 0, 0.1),
|
| 564 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
| 565 |
-
cursor: pointer;
|
| 566 |
-
}
|
| 567 |
-
|
| 568 |
-
.status-pill:hover {
|
| 569 |
-
box-shadow:
|
| 570 |
-
0 4px 12px rgba(0, 0, 0, 0.15),
|
| 571 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
| 572 |
-
border-color: rgba(0, 0, 0, 0.15);
|
| 573 |
-
}
|
| 574 |
-
|
| 575 |
-
.status-pill::before {
|
| 576 |
-
content: '';
|
| 577 |
-
position: absolute;
|
| 578 |
-
inset: 0;
|
| 579 |
-
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
|
| 580 |
-
opacity: 0;
|
| 581 |
-
transition: opacity 0.3s ease;
|
| 582 |
-
}
|
| 583 |
-
|
| 584 |
-
.status-pill:hover::before {
|
| 585 |
-
opacity: 1;
|
| 586 |
}
|
| 587 |
|
| 588 |
.status-dot {
|
| 589 |
-
width:
|
| 590 |
-
height:
|
| 591 |
border-radius: 50%;
|
| 592 |
background: var(--warning);
|
| 593 |
-
position: relative;
|
| 594 |
-
flex-shrink: 0;
|
| 595 |
-
box-shadow:
|
| 596 |
-
0 0 8px var(--warning),
|
| 597 |
-
0 2px 4px rgba(0, 0, 0, 0.2);
|
| 598 |
-
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
| 599 |
-
border: 2px solid rgba(255, 255, 255, 0.9);
|
| 600 |
-
}
|
| 601 |
-
|
| 602 |
-
.status-pill:hover .status-dot {
|
| 603 |
-
box-shadow:
|
| 604 |
-
0 0 12px var(--warning),
|
| 605 |
-
0 2px 6px rgba(0, 0, 0, 0.25);
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
.status-pill[data-state='ok'] {
|
| 609 |
-
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
| 610 |
-
border: 2px solid #10b981;
|
| 611 |
-
color: #ffffff;
|
| 612 |
-
box-shadow:
|
| 613 |
-
0 2px 8px rgba(16, 185, 129, 0.3),
|
| 614 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 615 |
-
font-weight: 700;
|
| 616 |
-
position: relative;
|
| 617 |
-
}
|
| 618 |
-
|
| 619 |
-
.status-pill[data-state='ok']:hover {
|
| 620 |
-
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
| 621 |
-
box-shadow:
|
| 622 |
-
0 4px 12px rgba(16, 185, 129, 0.4),
|
| 623 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
| 624 |
-
}
|
| 625 |
-
|
| 626 |
-
@keyframes live-pulse {
|
| 627 |
-
0%, 100% {
|
| 628 |
-
box-shadow:
|
| 629 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.2),
|
| 630 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.3),
|
| 631 |
-
0 4px 16px rgba(34, 197, 94, 0.4),
|
| 632 |
-
0 0 30px rgba(34, 197, 94, 0.3),
|
| 633 |
-
0 0 50px rgba(16, 185, 129, 0.2);
|
| 634 |
-
}
|
| 635 |
-
50% {
|
| 636 |
-
box-shadow:
|
| 637 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.25),
|
| 638 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.4),
|
| 639 |
-
0 6px 24px rgba(34, 197, 94, 0.5),
|
| 640 |
-
0 0 40px rgba(34, 197, 94, 0.4),
|
| 641 |
-
0 0 60px rgba(16, 185, 129, 0.3);
|
| 642 |
-
}
|
| 643 |
-
}
|
| 644 |
-
|
| 645 |
-
.status-pill[data-state='ok']::before {
|
| 646 |
-
content: '';
|
| 647 |
-
position: absolute;
|
| 648 |
-
top: 2px;
|
| 649 |
-
left: 2px;
|
| 650 |
-
right: 2px;
|
| 651 |
-
height: 50%;
|
| 652 |
-
border-radius: 999px 999px 0 0;
|
| 653 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
|
| 654 |
-
pointer-events: none;
|
| 655 |
}
|
| 656 |
|
| 657 |
.status-pill[data-state='ok'] .status-dot {
|
| 658 |
-
background:
|
| 659 |
-
border: 2px solid #10b981;
|
| 660 |
-
box-shadow:
|
| 661 |
-
0 0 8px rgba(16, 185, 129, 0.6),
|
| 662 |
-
0 2px 4px rgba(0, 0, 0, 0.2),
|
| 663 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
.status-pill[data-state='ok']:hover .status-dot {
|
| 667 |
-
box-shadow:
|
| 668 |
-
0 0 12px rgba(16, 185, 129, 0.8),
|
| 669 |
-
0 2px 6px rgba(0, 0, 0, 0.25),
|
| 670 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.9);
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
@keyframes live-dot-pulse {
|
| 674 |
-
0%, 100% {
|
| 675 |
-
transform: scale(1);
|
| 676 |
-
box-shadow:
|
| 677 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.4),
|
| 678 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.4),
|
| 679 |
-
0 0 16px rgba(34, 197, 94, 0.8),
|
| 680 |
-
0 0 32px rgba(34, 197, 94, 0.6),
|
| 681 |
-
0 0 48px rgba(16, 185, 129, 0.4);
|
| 682 |
-
}
|
| 683 |
-
50% {
|
| 684 |
-
transform: scale(1.15);
|
| 685 |
-
box-shadow:
|
| 686 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.5),
|
| 687 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.5),
|
| 688 |
-
0 0 20px rgba(34, 197, 94, 1),
|
| 689 |
-
0 0 40px rgba(34, 197, 94, 0.8),
|
| 690 |
-
0 0 60px rgba(16, 185, 129, 0.6);
|
| 691 |
-
}
|
| 692 |
-
}
|
| 693 |
-
|
| 694 |
-
.status-pill[data-state='ok']::after {
|
| 695 |
-
display: none;
|
| 696 |
-
}
|
| 697 |
-
|
| 698 |
-
.status-pill[data-state='warn'] {
|
| 699 |
-
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
| 700 |
-
border: 2px solid #f59e0b;
|
| 701 |
-
color: #ffffff;
|
| 702 |
-
box-shadow:
|
| 703 |
-
0 2px 8px rgba(245, 158, 11, 0.3),
|
| 704 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
.status-pill[data-state='warn']:hover {
|
| 708 |
-
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
|
| 709 |
-
box-shadow:
|
| 710 |
-
0 4px 12px rgba(245, 158, 11, 0.4),
|
| 711 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
| 712 |
}
|
| 713 |
|
| 714 |
.status-pill[data-state='warn'] .status-dot {
|
| 715 |
-
background:
|
| 716 |
-
border: 2px solid #f59e0b;
|
| 717 |
-
box-shadow:
|
| 718 |
-
0 0 8px rgba(245, 158, 11, 0.6),
|
| 719 |
-
0 2px 4px rgba(0, 0, 0, 0.2),
|
| 720 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 721 |
-
}
|
| 722 |
-
|
| 723 |
-
.status-pill[data-state='warn']:hover .status-dot {
|
| 724 |
-
box-shadow:
|
| 725 |
-
0 0 12px rgba(245, 158, 11, 0.8),
|
| 726 |
-
0 2px 6px rgba(0, 0, 0, 0.25),
|
| 727 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.9);
|
| 728 |
-
}
|
| 729 |
-
|
| 730 |
-
.status-pill[data-state='error'] {
|
| 731 |
-
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
| 732 |
-
border: 2px solid #ef4444;
|
| 733 |
-
color: #ffffff;
|
| 734 |
-
box-shadow:
|
| 735 |
-
0 2px 8px rgba(239, 68, 68, 0.3),
|
| 736 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 737 |
-
}
|
| 738 |
-
|
| 739 |
-
.status-pill[data-state='error']:hover {
|
| 740 |
-
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
| 741 |
-
box-shadow:
|
| 742 |
-
0 4px 12px rgba(239, 68, 68, 0.4),
|
| 743 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
| 744 |
}
|
| 745 |
|
| 746 |
.status-pill[data-state='error'] .status-dot {
|
| 747 |
-
background:
|
| 748 |
-
border: 2px solid #ef4444;
|
| 749 |
-
box-shadow:
|
| 750 |
-
0 0 8px rgba(239, 68, 68, 0.6),
|
| 751 |
-
0 2px 4px rgba(0, 0, 0, 0.2),
|
| 752 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.8);
|
| 753 |
-
}
|
| 754 |
-
|
| 755 |
-
.status-pill[data-state='error']:hover .status-dot {
|
| 756 |
-
box-shadow:
|
| 757 |
-
0 0 12px rgba(239, 68, 68, 0.8),
|
| 758 |
-
0 2px 6px rgba(0, 0, 0, 0.25),
|
| 759 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.9);
|
| 760 |
-
}
|
| 761 |
-
|
| 762 |
-
@keyframes pulse-green {
|
| 763 |
-
0%, 100% {
|
| 764 |
-
transform: scale(1);
|
| 765 |
-
opacity: 1;
|
| 766 |
-
box-shadow:
|
| 767 |
-
0 0 16px #86efac,
|
| 768 |
-
0 0 32px rgba(74, 222, 128, 0.8),
|
| 769 |
-
0 0 48px rgba(34, 197, 94, 0.6);
|
| 770 |
-
}
|
| 771 |
-
50% {
|
| 772 |
-
transform: scale(1.3);
|
| 773 |
-
opacity: 0.9;
|
| 774 |
-
box-shadow:
|
| 775 |
-
0 0 24px #86efac,
|
| 776 |
-
0 0 48px rgba(74, 222, 128, 1),
|
| 777 |
-
0 0 72px rgba(34, 197, 94, 0.8);
|
| 778 |
-
}
|
| 779 |
-
}
|
| 780 |
-
|
| 781 |
-
@keyframes glow-pulse {
|
| 782 |
-
0%, 100% {
|
| 783 |
-
opacity: 0.6;
|
| 784 |
-
transform: scale(1);
|
| 785 |
-
}
|
| 786 |
-
50% {
|
| 787 |
-
opacity: 1;
|
| 788 |
-
transform: scale(1.1);
|
| 789 |
-
}
|
| 790 |
}
|
| 791 |
|
| 792 |
.page-container {
|
|
@@ -810,117 +208,18 @@ body[data-theme='light'] {
|
|
| 810 |
}
|
| 811 |
|
| 812 |
.section-title {
|
| 813 |
-
font-size: 1.
|
| 814 |
-
|
| 815 |
-
letter-spacing: -0.025em;
|
| 816 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 817 |
-
margin: 0;
|
| 818 |
-
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
|
| 819 |
-
-webkit-background-clip: text;
|
| 820 |
-
-webkit-text-fill-color: transparent;
|
| 821 |
-
background-clip: text;
|
| 822 |
}
|
| 823 |
|
| 824 |
.glass-card {
|
| 825 |
-
background:
|
| 826 |
-
|
| 827 |
-
-webkit-backdrop-filter: blur(35px) saturate(180%);
|
| 828 |
-
border: 2px solid rgba(255, 255, 255, 0.22);
|
| 829 |
border-radius: 24px;
|
| 830 |
-
padding:
|
| 831 |
-
box-shadow:
|
| 832 |
-
0 8px 32px rgba(0, 0, 0, 0.5),
|
| 833 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.25),
|
| 834 |
-
inset 0 -1px 0 rgba(0, 0, 0, 0.3),
|
| 835 |
-
0 0 40px rgba(0, 212, 255, 0.15);
|
| 836 |
position: relative;
|
| 837 |
-
overflow:
|
| 838 |
-
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 839 |
-
}
|
| 840 |
-
|
| 841 |
-
.glass-card::before {
|
| 842 |
-
content: '';
|
| 843 |
-
position: absolute;
|
| 844 |
-
inset: -4px;
|
| 845 |
-
background: linear-gradient(135deg,
|
| 846 |
-
rgba(0, 212, 255, 0.3) 0%,
|
| 847 |
-
rgba(139, 92, 246, 0.3) 50%,
|
| 848 |
-
rgba(236, 72, 153, 0.3) 100%);
|
| 849 |
-
border-radius: 28px;
|
| 850 |
-
opacity: 0;
|
| 851 |
-
transition: opacity 0.4s ease;
|
| 852 |
-
z-index: -1;
|
| 853 |
-
filter: blur(20px);
|
| 854 |
-
animation: card-glow-pulse 4s ease-in-out infinite;
|
| 855 |
-
}
|
| 856 |
-
|
| 857 |
-
@keyframes card-glow-pulse {
|
| 858 |
-
0%, 100% {
|
| 859 |
-
opacity: 0;
|
| 860 |
-
filter: blur(20px);
|
| 861 |
-
}
|
| 862 |
-
50% {
|
| 863 |
-
opacity: 0.4;
|
| 864 |
-
filter: blur(25px);
|
| 865 |
-
}
|
| 866 |
-
}
|
| 867 |
-
|
| 868 |
-
.glass-card::after {
|
| 869 |
-
content: '';
|
| 870 |
-
position: absolute;
|
| 871 |
-
top: 0;
|
| 872 |
-
left: 0;
|
| 873 |
-
right: 0;
|
| 874 |
-
height: 3px;
|
| 875 |
-
background: linear-gradient(90deg,
|
| 876 |
-
transparent,
|
| 877 |
-
rgba(0, 212, 255, 0.7),
|
| 878 |
-
rgba(139, 92, 246, 0.7),
|
| 879 |
-
rgba(236, 72, 153, 0.7),
|
| 880 |
-
transparent);
|
| 881 |
-
border-radius: 24px 24px 0 0;
|
| 882 |
-
opacity: 0.7;
|
| 883 |
-
animation: card-shimmer 4s infinite;
|
| 884 |
-
}
|
| 885 |
-
|
| 886 |
-
@keyframes card-shimmer {
|
| 887 |
-
0%, 100% { opacity: 0.7; }
|
| 888 |
-
50% { opacity: 1; }
|
| 889 |
-
}
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
.glass-card:hover {
|
| 893 |
-
box-shadow:
|
| 894 |
-
0 16px 48px rgba(0, 0, 0, 0.6),
|
| 895 |
-
0 0 50px rgba(0, 212, 255, 0.5),
|
| 896 |
-
0 0 100px rgba(139, 92, 246, 0.4),
|
| 897 |
-
0 0 150px rgba(236, 72, 153, 0.3),
|
| 898 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
| 899 |
-
inset 0 -1px 0 rgba(0, 0, 0, 0.4);
|
| 900 |
-
border-color: rgba(0, 212, 255, 0.6);
|
| 901 |
-
}
|
| 902 |
-
|
| 903 |
-
.glass-card:hover::before {
|
| 904 |
-
opacity: 0.8;
|
| 905 |
-
filter: blur(30px);
|
| 906 |
-
}
|
| 907 |
-
|
| 908 |
-
.glass-card:hover::after {
|
| 909 |
-
opacity: 1;
|
| 910 |
-
height: 4px;
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
.glass-card h4 {
|
| 914 |
-
font-size: 1.25rem;
|
| 915 |
-
font-weight: 700;
|
| 916 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 917 |
-
margin: 0 0 20px 0;
|
| 918 |
-
color: var(--text-primary);
|
| 919 |
-
letter-spacing: -0.02em;
|
| 920 |
-
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
|
| 921 |
-
-webkit-background-clip: text;
|
| 922 |
-
-webkit-text-fill-color: transparent;
|
| 923 |
-
background-clip: text;
|
| 924 |
}
|
| 925 |
|
| 926 |
.glass-card::before {
|
|
@@ -943,379 +242,24 @@ body[data-theme='light'] {
|
|
| 943 |
margin-bottom: 24px;
|
| 944 |
}
|
| 945 |
|
| 946 |
-
.stat-card {
|
| 947 |
-
display: flex;
|
| 948 |
-
flex-direction: column;
|
| 949 |
-
gap: 1rem;
|
| 950 |
-
position: relative;
|
| 951 |
-
background: rgba(255, 255, 255, 0.08);
|
| 952 |
-
backdrop-filter: blur(30px) saturate(180%);
|
| 953 |
-
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
| 954 |
-
border: 2px solid rgba(255, 255, 255, 0.15);
|
| 955 |
-
border-radius: 20px;
|
| 956 |
-
padding: 20px;
|
| 957 |
-
box-shadow:
|
| 958 |
-
0 8px 32px rgba(0, 0, 0, 0.4),
|
| 959 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
| 960 |
-
inset 0 -1px 0 rgba(0, 0, 0, 0.2);
|
| 961 |
-
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 962 |
-
overflow: visible;
|
| 963 |
-
}
|
| 964 |
-
|
| 965 |
-
.stat-card::before {
|
| 966 |
-
content: '';
|
| 967 |
-
position: absolute;
|
| 968 |
-
inset: -4px;
|
| 969 |
-
border-radius: 24px;
|
| 970 |
-
background: linear-gradient(135deg,
|
| 971 |
-
rgba(0, 212, 255, 0.4) 0%,
|
| 972 |
-
rgba(139, 92, 246, 0.4) 50%,
|
| 973 |
-
rgba(236, 72, 153, 0.4) 100%);
|
| 974 |
-
opacity: 0;
|
| 975 |
-
transition: opacity 0.4s ease;
|
| 976 |
-
z-index: -1;
|
| 977 |
-
filter: blur(20px);
|
| 978 |
-
animation: glow-pulse 3s ease-in-out infinite;
|
| 979 |
-
}
|
| 980 |
-
|
| 981 |
-
.stat-card::after {
|
| 982 |
-
content: '';
|
| 983 |
-
position: absolute;
|
| 984 |
-
top: 0;
|
| 985 |
-
left: 0;
|
| 986 |
-
right: 0;
|
| 987 |
-
height: 3px;
|
| 988 |
-
background: linear-gradient(90deg,
|
| 989 |
-
transparent,
|
| 990 |
-
rgba(0, 212, 255, 0.8),
|
| 991 |
-
rgba(139, 92, 246, 0.8),
|
| 992 |
-
rgba(236, 72, 153, 0.8),
|
| 993 |
-
transparent);
|
| 994 |
-
border-radius: 20px 20px 0 0;
|
| 995 |
-
opacity: 0.6;
|
| 996 |
-
animation: shimmer 3s infinite;
|
| 997 |
-
}
|
| 998 |
-
|
| 999 |
-
@keyframes glow-pulse {
|
| 1000 |
-
0%, 100% {
|
| 1001 |
-
opacity: 0;
|
| 1002 |
-
filter: blur(20px);
|
| 1003 |
-
}
|
| 1004 |
-
50% {
|
| 1005 |
-
opacity: 0.5;
|
| 1006 |
-
filter: blur(25px);
|
| 1007 |
-
}
|
| 1008 |
-
}
|
| 1009 |
-
|
| 1010 |
-
@keyframes shimmer {
|
| 1011 |
-
0%, 100% { opacity: 0.6; }
|
| 1012 |
-
50% { opacity: 1; }
|
| 1013 |
-
}
|
| 1014 |
-
|
| 1015 |
-
.stat-card:hover {
|
| 1016 |
-
border-color: rgba(0, 212, 255, 0.5);
|
| 1017 |
-
box-shadow:
|
| 1018 |
-
0 16px 48px rgba(0, 0, 0, 0.5),
|
| 1019 |
-
0 0 40px rgba(0, 212, 255, 0.4),
|
| 1020 |
-
0 0 80px rgba(139, 92, 246, 0.3),
|
| 1021 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3),
|
| 1022 |
-
inset 0 -1px 0 rgba(0, 0, 0, 0.3);
|
| 1023 |
-
}
|
| 1024 |
-
|
| 1025 |
-
.stat-card:hover::before {
|
| 1026 |
-
opacity: 0.6;
|
| 1027 |
-
filter: blur(25px);
|
| 1028 |
-
}
|
| 1029 |
-
|
| 1030 |
-
.stat-card:hover::after {
|
| 1031 |
-
opacity: 1;
|
| 1032 |
-
height: 4px;
|
| 1033 |
-
}
|
| 1034 |
-
|
| 1035 |
-
.stat-header {
|
| 1036 |
-
display: flex;
|
| 1037 |
-
align-items: center;
|
| 1038 |
-
gap: 0.75rem;
|
| 1039 |
-
}
|
| 1040 |
-
|
| 1041 |
-
.stat-icon {
|
| 1042 |
-
display: flex;
|
| 1043 |
-
align-items: center;
|
| 1044 |
-
justify-content: center;
|
| 1045 |
-
width: 52px;
|
| 1046 |
-
height: 52px;
|
| 1047 |
-
border-radius: 14px;
|
| 1048 |
-
background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(139, 92, 246, 0.2));
|
| 1049 |
-
flex-shrink: 0;
|
| 1050 |
-
border: 2px solid rgba(0, 212, 255, 0.3);
|
| 1051 |
-
box-shadow:
|
| 1052 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.2),
|
| 1053 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.3),
|
| 1054 |
-
0 4px 12px rgba(0, 212, 255, 0.3),
|
| 1055 |
-
0 0 20px rgba(0, 212, 255, 0.2);
|
| 1056 |
-
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1057 |
-
position: relative;
|
| 1058 |
-
color: #00D4FF;
|
| 1059 |
-
overflow: visible;
|
| 1060 |
-
}
|
| 1061 |
-
|
| 1062 |
-
.stat-icon::after {
|
| 1063 |
-
content: '';
|
| 1064 |
-
position: absolute;
|
| 1065 |
-
inset: -2px;
|
| 1066 |
-
border-radius: 16px;
|
| 1067 |
-
background: linear-gradient(135deg, rgba(0, 212, 255, 0.4), rgba(139, 92, 246, 0.4));
|
| 1068 |
-
opacity: 0;
|
| 1069 |
-
filter: blur(12px);
|
| 1070 |
-
transition: opacity 0.4s ease;
|
| 1071 |
-
z-index: -1;
|
| 1072 |
-
}
|
| 1073 |
-
|
| 1074 |
-
.stat-icon::before {
|
| 1075 |
-
content: '';
|
| 1076 |
-
position: absolute;
|
| 1077 |
-
top: 2px;
|
| 1078 |
-
left: 2px;
|
| 1079 |
-
right: 2px;
|
| 1080 |
-
height: 50%;
|
| 1081 |
-
border-radius: 12px 12px 0 0;
|
| 1082 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.15), transparent);
|
| 1083 |
-
pointer-events: none;
|
| 1084 |
-
}
|
| 1085 |
-
|
| 1086 |
-
.stat-icon svg {
|
| 1087 |
-
position: relative;
|
| 1088 |
-
z-index: 1;
|
| 1089 |
-
width: 22px;
|
| 1090 |
-
height: 22px;
|
| 1091 |
-
opacity: 0.85;
|
| 1092 |
-
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
| 1093 |
-
}
|
| 1094 |
-
|
| 1095 |
-
.stat-card:hover .stat-icon {
|
| 1096 |
-
box-shadow:
|
| 1097 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.25),
|
| 1098 |
-
inset 0 -1px 2px rgba(0, 0, 0, 0.4),
|
| 1099 |
-
0 8px 24px rgba(0, 212, 255, 0.5),
|
| 1100 |
-
0 0 40px rgba(0, 212, 255, 0.4),
|
| 1101 |
-
0 0 60px rgba(139, 92, 246, 0.3);
|
| 1102 |
-
border-color: rgba(0, 212, 255, 0.6);
|
| 1103 |
-
background: linear-gradient(135deg, rgba(0, 212, 255, 0.3), rgba(139, 92, 246, 0.3));
|
| 1104 |
-
}
|
| 1105 |
-
|
| 1106 |
-
.stat-card:hover .stat-icon::after {
|
| 1107 |
-
opacity: 0.8;
|
| 1108 |
-
filter: blur(16px);
|
| 1109 |
-
}
|
| 1110 |
-
|
| 1111 |
-
.stat-card:hover .stat-icon svg {
|
| 1112 |
-
opacity: 1;
|
| 1113 |
-
}
|
| 1114 |
-
|
| 1115 |
.stat-card h3 {
|
| 1116 |
-
font-size: 0.
|
| 1117 |
-
font-weight: 700;
|
| 1118 |
text-transform: uppercase;
|
| 1119 |
-
letter-spacing: 0.
|
| 1120 |
-
color:
|
| 1121 |
-
margin: 0;
|
| 1122 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 1123 |
-
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 1124 |
}
|
| 1125 |
|
| 1126 |
.stat-value {
|
| 1127 |
-
font-size:
|
| 1128 |
-
font-weight: 800;
|
| 1129 |
-
margin: 0;
|
| 1130 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 1131 |
-
letter-spacing: -0.03em;
|
| 1132 |
-
line-height: 1.1;
|
| 1133 |
-
background: linear-gradient(135deg, #ffffff 0%, #00D4FF 50%, #8B5CF6 100%);
|
| 1134 |
-
-webkit-background-clip: text;
|
| 1135 |
-
-webkit-text-fill-color: transparent;
|
| 1136 |
-
background-clip: text;
|
| 1137 |
-
position: relative;
|
| 1138 |
-
filter: drop-shadow(0 0 8px rgba(0, 212, 255, 0.4));
|
| 1139 |
-
text-shadow:
|
| 1140 |
-
0 2px 12px rgba(0, 0, 0, 0.5),
|
| 1141 |
-
0 0 30px rgba(0, 212, 255, 0.3);
|
| 1142 |
-
}
|
| 1143 |
-
|
| 1144 |
-
.stat-card:hover .stat-value {
|
| 1145 |
-
filter: drop-shadow(0 0 12px rgba(0, 212, 255, 0.6)) drop-shadow(0 0 24px rgba(139, 92, 246, 0.4));
|
| 1146 |
-
background: linear-gradient(135deg, #ffffff 0%, #00D4FF 30%, #8B5CF6 70%, #EC4899 100%);
|
| 1147 |
-
-webkit-background-clip: text;
|
| 1148 |
-
-webkit-text-fill-color: transparent;
|
| 1149 |
-
background-clip: text;
|
| 1150 |
-
}
|
| 1151 |
-
|
| 1152 |
-
.stat-value-wrapper {
|
| 1153 |
-
display: flex;
|
| 1154 |
-
flex-direction: column;
|
| 1155 |
-
gap: 0.5rem;
|
| 1156 |
-
margin: 0.5rem 0;
|
| 1157 |
-
}
|
| 1158 |
-
|
| 1159 |
-
.stat-change {
|
| 1160 |
-
display: inline-flex;
|
| 1161 |
-
align-items: center;
|
| 1162 |
-
gap: 0.375rem;
|
| 1163 |
-
font-size: 0.8125rem;
|
| 1164 |
-
font-weight: 600;
|
| 1165 |
-
font-family: 'Manrope', sans-serif;
|
| 1166 |
-
width: fit-content;
|
| 1167 |
-
transition: all 0.2s ease;
|
| 1168 |
-
}
|
| 1169 |
-
|
| 1170 |
-
.change-icon-wrapper {
|
| 1171 |
-
display: flex;
|
| 1172 |
-
align-items: center;
|
| 1173 |
-
justify-content: center;
|
| 1174 |
-
width: 16px;
|
| 1175 |
-
height: 16px;
|
| 1176 |
-
flex-shrink: 0;
|
| 1177 |
-
opacity: 0.8;
|
| 1178 |
-
}
|
| 1179 |
-
|
| 1180 |
-
.change-icon-wrapper.positive {
|
| 1181 |
-
color: #22c55e;
|
| 1182 |
-
}
|
| 1183 |
-
|
| 1184 |
-
.change-icon-wrapper.negative {
|
| 1185 |
-
color: #ef4444;
|
| 1186 |
-
}
|
| 1187 |
-
|
| 1188 |
-
.stat-change.positive {
|
| 1189 |
-
color: #4ade80;
|
| 1190 |
-
background: rgba(34, 197, 94, 0.2);
|
| 1191 |
-
padding: 4px 10px;
|
| 1192 |
-
border-radius: 8px;
|
| 1193 |
-
border: 1px solid rgba(34, 197, 94, 0.4);
|
| 1194 |
-
box-shadow:
|
| 1195 |
-
0 2px 8px rgba(34, 197, 94, 0.3),
|
| 1196 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 1197 |
-
text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
| 1198 |
-
font-weight: 700;
|
| 1199 |
-
}
|
| 1200 |
-
|
| 1201 |
-
.stat-change.negative {
|
| 1202 |
-
color: #f87171;
|
| 1203 |
-
background: rgba(239, 68, 68, 0.2);
|
| 1204 |
-
padding: 4px 10px;
|
| 1205 |
-
border-radius: 8px;
|
| 1206 |
-
border: 1px solid rgba(239, 68, 68, 0.4);
|
| 1207 |
-
box-shadow:
|
| 1208 |
-
0 2px 8px rgba(239, 68, 68, 0.3),
|
| 1209 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 1210 |
-
text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
| 1211 |
-
font-weight: 700;
|
| 1212 |
-
}
|
| 1213 |
-
|
| 1214 |
-
.change-value {
|
| 1215 |
font-weight: 600;
|
| 1216 |
-
|
| 1217 |
-
}
|
| 1218 |
-
|
| 1219 |
-
.stat-metrics {
|
| 1220 |
-
display: flex;
|
| 1221 |
-
gap: 1rem;
|
| 1222 |
-
margin-top: auto;
|
| 1223 |
-
padding-top: 1rem;
|
| 1224 |
-
border-top: 2px solid rgba(255, 255, 255, 0.12);
|
| 1225 |
-
background: linear-gradient(90deg,
|
| 1226 |
-
transparent,
|
| 1227 |
-
rgba(0, 212, 255, 0.05),
|
| 1228 |
-
rgba(139, 92, 246, 0.05),
|
| 1229 |
-
transparent);
|
| 1230 |
-
margin-left: -20px;
|
| 1231 |
-
margin-right: -20px;
|
| 1232 |
-
padding-left: 20px;
|
| 1233 |
-
padding-right: 20px;
|
| 1234 |
-
border-radius: 0 0 20px 20px;
|
| 1235 |
-
}
|
| 1236 |
-
|
| 1237 |
-
.stat-metric {
|
| 1238 |
-
display: flex;
|
| 1239 |
-
flex-direction: column;
|
| 1240 |
-
gap: 0.25rem;
|
| 1241 |
-
flex: 1;
|
| 1242 |
-
}
|
| 1243 |
-
|
| 1244 |
-
.stat-metric .metric-label {
|
| 1245 |
-
font-size: 0.7rem;
|
| 1246 |
-
text-transform: uppercase;
|
| 1247 |
-
letter-spacing: 0.1em;
|
| 1248 |
-
color: rgba(255, 255, 255, 0.6);
|
| 1249 |
-
font-weight: 700;
|
| 1250 |
-
font-family: 'Manrope', sans-serif;
|
| 1251 |
-
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
| 1252 |
-
}
|
| 1253 |
-
|
| 1254 |
-
.stat-metric .metric-value {
|
| 1255 |
-
font-size: 0.9375rem;
|
| 1256 |
-
font-weight: 700;
|
| 1257 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 1258 |
-
color: rgba(255, 255, 255, 0.9);
|
| 1259 |
-
display: flex;
|
| 1260 |
-
align-items: center;
|
| 1261 |
-
gap: 6px;
|
| 1262 |
-
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
| 1263 |
-
}
|
| 1264 |
-
|
| 1265 |
-
.metric-icon {
|
| 1266 |
-
display: inline-flex;
|
| 1267 |
-
align-items: center;
|
| 1268 |
-
justify-content: center;
|
| 1269 |
-
width: 18px;
|
| 1270 |
-
height: 18px;
|
| 1271 |
-
border-radius: 4px;
|
| 1272 |
-
font-size: 0.75rem;
|
| 1273 |
-
font-weight: 700;
|
| 1274 |
-
flex-shrink: 0;
|
| 1275 |
-
}
|
| 1276 |
-
|
| 1277 |
-
.metric-icon.positive {
|
| 1278 |
-
background: rgba(34, 197, 94, 0.3);
|
| 1279 |
-
color: #4ade80;
|
| 1280 |
-
border: 1px solid rgba(34, 197, 94, 0.5);
|
| 1281 |
-
box-shadow:
|
| 1282 |
-
0 2px 8px rgba(34, 197, 94, 0.4),
|
| 1283 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 1284 |
-
text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
| 1285 |
-
}
|
| 1286 |
-
|
| 1287 |
-
.metric-icon.negative {
|
| 1288 |
-
background: rgba(239, 68, 68, 0.3);
|
| 1289 |
-
color: #f87171;
|
| 1290 |
-
border: 1px solid rgba(239, 68, 68, 0.5);
|
| 1291 |
-
box-shadow:
|
| 1292 |
-
0 2px 8px rgba(239, 68, 68, 0.4),
|
| 1293 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 1294 |
-
text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
| 1295 |
-
}
|
| 1296 |
-
|
| 1297 |
-
.stat-metric .metric-value.positive {
|
| 1298 |
-
color: #4ade80;
|
| 1299 |
-
text-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
|
| 1300 |
-
font-weight: 800;
|
| 1301 |
-
}
|
| 1302 |
-
|
| 1303 |
-
.stat-metric .metric-value.negative {
|
| 1304 |
-
color: #f87171;
|
| 1305 |
-
text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
|
| 1306 |
-
font-weight: 800;
|
| 1307 |
}
|
| 1308 |
|
| 1309 |
.stat-trend {
|
| 1310 |
display: flex;
|
| 1311 |
align-items: center;
|
| 1312 |
gap: 6px;
|
| 1313 |
-
font-size: 0.
|
| 1314 |
-
color: var(--text-faint);
|
| 1315 |
-
font-family: 'Manrope', sans-serif;
|
| 1316 |
-
font-weight: 500;
|
| 1317 |
-
margin-top: auto;
|
| 1318 |
-
letter-spacing: 0.02em;
|
| 1319 |
}
|
| 1320 |
|
| 1321 |
.grid-two {
|
|
@@ -1324,153 +268,35 @@ body[data-theme='light'] {
|
|
| 1324 |
gap: 20px;
|
| 1325 |
}
|
| 1326 |
|
| 1327 |
-
.grid-three {
|
| 1328 |
-
display: grid;
|
| 1329 |
-
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 1330 |
-
gap: 18px;
|
| 1331 |
-
}
|
| 1332 |
-
|
| 1333 |
-
.grid-four {
|
| 1334 |
-
display: grid;
|
| 1335 |
-
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
| 1336 |
-
gap: 18px;
|
| 1337 |
-
}
|
| 1338 |
-
|
| 1339 |
.table-wrapper {
|
| 1340 |
overflow: auto;
|
| 1341 |
}
|
| 1342 |
|
| 1343 |
table {
|
| 1344 |
width: 100%;
|
| 1345 |
-
border-collapse:
|
| 1346 |
-
border-spacing: 0;
|
| 1347 |
}
|
| 1348 |
|
| 1349 |
th, td {
|
| 1350 |
text-align: left;
|
| 1351 |
-
padding: 12px
|
| 1352 |
-
font-size: 0.
|
| 1353 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 1354 |
}
|
| 1355 |
|
| 1356 |
th {
|
| 1357 |
-
font-size: 0.
|
| 1358 |
-
|
| 1359 |
-
letter-spacing: 0.06em;
|
| 1360 |
color: var(--text-muted);
|
| 1361 |
text-transform: uppercase;
|
| 1362 |
-
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
| 1363 |
-
background: rgba(255, 255, 255, 0.03);
|
| 1364 |
-
position: sticky;
|
| 1365 |
-
top: 0;
|
| 1366 |
-
z-index: 10;
|
| 1367 |
-
white-space: nowrap;
|
| 1368 |
-
}
|
| 1369 |
-
|
| 1370 |
-
th:first-child {
|
| 1371 |
-
border-top-left-radius: 12px;
|
| 1372 |
-
padding-left: 16px;
|
| 1373 |
-
}
|
| 1374 |
-
|
| 1375 |
-
th:last-child {
|
| 1376 |
-
border-top-right-radius: 12px;
|
| 1377 |
-
padding-right: 16px;
|
| 1378 |
-
}
|
| 1379 |
-
|
| 1380 |
-
td {
|
| 1381 |
-
font-weight: 500;
|
| 1382 |
-
color: var(--text-primary);
|
| 1383 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 1384 |
-
vertical-align: middle;
|
| 1385 |
-
}
|
| 1386 |
-
|
| 1387 |
-
td:first-child {
|
| 1388 |
-
padding-left: 16px;
|
| 1389 |
-
font-weight: 600;
|
| 1390 |
-
color: var(--text-muted);
|
| 1391 |
-
font-size: 0.75rem;
|
| 1392 |
-
}
|
| 1393 |
-
|
| 1394 |
-
td:last-child {
|
| 1395 |
-
padding-right: 16px;
|
| 1396 |
}
|
| 1397 |
|
| 1398 |
tr {
|
| 1399 |
-
transition:
|
| 1400 |
-
}
|
| 1401 |
-
|
| 1402 |
-
tbody tr {
|
| 1403 |
-
border-left: 2px solid transparent;
|
| 1404 |
-
transition: all 0.2s ease;
|
| 1405 |
}
|
| 1406 |
|
| 1407 |
tbody tr:hover {
|
| 1408 |
-
background: rgba(255, 255, 255, 0.
|
| 1409 |
-
|
| 1410 |
-
transform: translateX(2px);
|
| 1411 |
-
}
|
| 1412 |
-
|
| 1413 |
-
tbody tr:last-child td:first-child {
|
| 1414 |
-
border-bottom-left-radius: 12px;
|
| 1415 |
-
}
|
| 1416 |
-
|
| 1417 |
-
tbody tr:last-child td:last-child {
|
| 1418 |
-
border-bottom-right-radius: 12px;
|
| 1419 |
-
}
|
| 1420 |
-
|
| 1421 |
-
tbody tr:last-child td {
|
| 1422 |
-
border-bottom: none;
|
| 1423 |
-
}
|
| 1424 |
-
|
| 1425 |
-
td.text-success,
|
| 1426 |
-
td.text-danger {
|
| 1427 |
-
display: flex;
|
| 1428 |
-
align-items: center;
|
| 1429 |
-
gap: 6px;
|
| 1430 |
-
font-weight: 600;
|
| 1431 |
-
font-size: 0.8125rem;
|
| 1432 |
-
}
|
| 1433 |
-
|
| 1434 |
-
td.text-success {
|
| 1435 |
-
color: #22c55e;
|
| 1436 |
-
}
|
| 1437 |
-
|
| 1438 |
-
td.text-danger {
|
| 1439 |
-
color: #ef4444;
|
| 1440 |
-
}
|
| 1441 |
-
|
| 1442 |
-
.table-change-icon {
|
| 1443 |
-
display: inline-flex;
|
| 1444 |
-
align-items: center;
|
| 1445 |
-
justify-content: center;
|
| 1446 |
-
width: 16px;
|
| 1447 |
-
height: 16px;
|
| 1448 |
-
flex-shrink: 0;
|
| 1449 |
-
opacity: 0.9;
|
| 1450 |
-
}
|
| 1451 |
-
|
| 1452 |
-
.table-change-icon.positive {
|
| 1453 |
-
color: #22c55e;
|
| 1454 |
-
}
|
| 1455 |
-
|
| 1456 |
-
.table-change-icon.negative {
|
| 1457 |
-
color: #ef4444;
|
| 1458 |
-
}
|
| 1459 |
-
|
| 1460 |
-
/* Chip styling for symbol column */
|
| 1461 |
-
.chip {
|
| 1462 |
-
display: inline-flex;
|
| 1463 |
-
align-items: center;
|
| 1464 |
-
padding: 4px 10px;
|
| 1465 |
-
background: rgba(143, 136, 255, 0.1);
|
| 1466 |
-
border: 1px solid rgba(143, 136, 255, 0.2);
|
| 1467 |
-
border-radius: 6px;
|
| 1468 |
-
font-size: 0.75rem;
|
| 1469 |
-
font-weight: 600;
|
| 1470 |
-
color: var(--primary-light);
|
| 1471 |
-
font-family: 'Manrope', sans-serif;
|
| 1472 |
-
letter-spacing: 0.02em;
|
| 1473 |
-
text-transform: uppercase;
|
| 1474 |
}
|
| 1475 |
|
| 1476 |
.badge {
|
|
@@ -1541,275 +367,51 @@ td.text-danger {
|
|
| 1541 |
|
| 1542 |
.input-chip {
|
| 1543 |
border: 1px solid var(--glass-border);
|
| 1544 |
-
background: rgba(255, 255, 255, 0.
|
| 1545 |
border-radius: 999px;
|
| 1546 |
padding: 8px 14px;
|
| 1547 |
color: var(--text-muted);
|
| 1548 |
display: inline-flex;
|
| 1549 |
align-items: center;
|
| 1550 |
gap: 10px;
|
| 1551 |
-
font-family: 'Inter', sans-serif;
|
| 1552 |
-
font-size: 0.875rem;
|
| 1553 |
}
|
| 1554 |
|
| 1555 |
-
|
| 1556 |
-
|
| 1557 |
-
|
| 1558 |
-
gap: 12px;
|
| 1559 |
-
align-items: center;
|
| 1560 |
-
margin-bottom: 20px;
|
| 1561 |
-
padding: 16px;
|
| 1562 |
-
background: var(--glass-bg);
|
| 1563 |
border: 1px solid var(--glass-border);
|
| 1564 |
-
border-radius:
|
| 1565 |
-
|
|
|
|
|
|
|
| 1566 |
}
|
| 1567 |
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
gap: 8px;
|
| 1571 |
-
flex-wrap: wrap;
|
| 1572 |
-
}
|
| 1573 |
-
|
| 1574 |
-
input[type='text'], select, textarea {
|
| 1575 |
-
width: 100%;
|
| 1576 |
-
background: rgba(255, 255, 255, 0.05);
|
| 1577 |
-
border: 1px solid var(--glass-border);
|
| 1578 |
-
border-radius: 12px;
|
| 1579 |
-
padding: 12px 16px;
|
| 1580 |
-
color: var(--text-primary);
|
| 1581 |
-
font-family: 'Inter', sans-serif;
|
| 1582 |
-
font-size: 0.9375rem;
|
| 1583 |
-
transition: all 0.2s ease;
|
| 1584 |
-
}
|
| 1585 |
-
|
| 1586 |
-
input[type='text']:focus, select:focus, textarea:focus {
|
| 1587 |
-
outline: none;
|
| 1588 |
-
border-color: var(--primary);
|
| 1589 |
-
background: rgba(255, 255, 255, 0.08);
|
| 1590 |
-
box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
|
| 1591 |
-
}
|
| 1592 |
-
|
| 1593 |
-
textarea {
|
| 1594 |
-
min-height: 100px;
|
| 1595 |
}
|
| 1596 |
|
| 1597 |
button.primary {
|
| 1598 |
background: linear-gradient(120deg, var(--primary), var(--secondary));
|
| 1599 |
border: none;
|
| 1600 |
-
border-radius:
|
| 1601 |
color: #fff;
|
| 1602 |
-
padding:
|
| 1603 |
-
font-weight:
|
| 1604 |
-
font-family: 'Manrope', sans-serif;
|
| 1605 |
-
font-size: 0.875rem;
|
| 1606 |
cursor: pointer;
|
| 1607 |
-
transition:
|
| 1608 |
-
box-shadow:
|
| 1609 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.1),
|
| 1610 |
-
0 2px 8px rgba(143, 136, 255, 0.2);
|
| 1611 |
-
position: relative;
|
| 1612 |
-
overflow: visible;
|
| 1613 |
-
display: flex;
|
| 1614 |
-
align-items: center;
|
| 1615 |
-
gap: 10px;
|
| 1616 |
-
}
|
| 1617 |
-
|
| 1618 |
-
button.primary::before {
|
| 1619 |
-
content: '';
|
| 1620 |
-
position: absolute;
|
| 1621 |
-
top: 0;
|
| 1622 |
-
left: 0;
|
| 1623 |
-
right: 0;
|
| 1624 |
-
height: 50%;
|
| 1625 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), transparent);
|
| 1626 |
-
border-radius: 12px 12px 0 0;
|
| 1627 |
-
pointer-events: none;
|
| 1628 |
}
|
| 1629 |
|
| 1630 |
button.primary:hover {
|
| 1631 |
-
|
| 1632 |
-
box-shadow:
|
| 1633 |
-
0 6px 20px rgba(59, 130, 246, 0.5),
|
| 1634 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.4),
|
| 1635 |
-
inset 0 -1px 0 rgba(0, 0, 0, 0.15);
|
| 1636 |
-
}
|
| 1637 |
-
|
| 1638 |
-
button.primary:hover::before {
|
| 1639 |
-
height: 50%;
|
| 1640 |
-
opacity: 1;
|
| 1641 |
-
}
|
| 1642 |
-
|
| 1643 |
-
button.primary:active {
|
| 1644 |
-
box-shadow:
|
| 1645 |
-
0 2px 8px rgba(59, 130, 246, 0.4),
|
| 1646 |
-
inset 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 1647 |
-
}
|
| 1648 |
-
|
| 1649 |
-
button.secondary {
|
| 1650 |
-
background: rgba(255, 255, 255, 0.95);
|
| 1651 |
-
border: 2px solid #3b82f6;
|
| 1652 |
-
border-radius: 12px;
|
| 1653 |
-
color: #3b82f6;
|
| 1654 |
-
padding: 14px 28px;
|
| 1655 |
-
font-weight: 700;
|
| 1656 |
-
font-family: 'Manrope', sans-serif;
|
| 1657 |
-
font-size: 0.875rem;
|
| 1658 |
-
cursor: pointer;
|
| 1659 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1660 |
-
position: relative;
|
| 1661 |
-
overflow: hidden;
|
| 1662 |
-
display: flex;
|
| 1663 |
-
align-items: center;
|
| 1664 |
-
gap: 10px;
|
| 1665 |
-
box-shadow:
|
| 1666 |
-
0 2px 8px rgba(59, 130, 246, 0.2),
|
| 1667 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
| 1668 |
-
}
|
| 1669 |
-
|
| 1670 |
-
button.secondary::before {
|
| 1671 |
-
content: '';
|
| 1672 |
-
position: absolute;
|
| 1673 |
-
top: 0;
|
| 1674 |
-
left: 0;
|
| 1675 |
-
right: 0;
|
| 1676 |
-
height: 50%;
|
| 1677 |
-
background: linear-gradient(180deg, rgba(59, 130, 246, 0.1), transparent);
|
| 1678 |
-
border-radius: 12px 12px 0 0;
|
| 1679 |
-
pointer-events: none;
|
| 1680 |
-
}
|
| 1681 |
-
|
| 1682 |
-
button.secondary::before {
|
| 1683 |
-
content: '';
|
| 1684 |
-
position: absolute;
|
| 1685 |
-
left: 0;
|
| 1686 |
-
top: 50%;
|
| 1687 |
-
transform: translateY(-50%);
|
| 1688 |
-
width: 2px;
|
| 1689 |
-
height: 0;
|
| 1690 |
-
background: var(--primary);
|
| 1691 |
-
border-radius: 0 2px 2px 0;
|
| 1692 |
-
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1693 |
-
opacity: 0;
|
| 1694 |
-
}
|
| 1695 |
-
|
| 1696 |
-
button.secondary::after {
|
| 1697 |
-
content: '';
|
| 1698 |
-
position: absolute;
|
| 1699 |
-
inset: 0;
|
| 1700 |
-
background: rgba(143, 136, 255, 0.05);
|
| 1701 |
-
border-radius: 10px;
|
| 1702 |
-
opacity: 0;
|
| 1703 |
-
transition: opacity 0.25s ease;
|
| 1704 |
-
z-index: -1;
|
| 1705 |
-
}
|
| 1706 |
-
|
| 1707 |
-
button.secondary:hover {
|
| 1708 |
-
background: #3b82f6;
|
| 1709 |
-
color: #ffffff;
|
| 1710 |
-
box-shadow:
|
| 1711 |
-
0 4px 16px rgba(59, 130, 246, 0.4),
|
| 1712 |
-
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
| 1713 |
-
}
|
| 1714 |
-
|
| 1715 |
-
button.secondary:hover::before {
|
| 1716 |
-
height: 50%;
|
| 1717 |
-
opacity: 1;
|
| 1718 |
-
}
|
| 1719 |
-
|
| 1720 |
-
button.secondary:hover::after {
|
| 1721 |
-
opacity: 1;
|
| 1722 |
-
}
|
| 1723 |
-
|
| 1724 |
-
button.secondary.active {
|
| 1725 |
-
background: rgba(143, 136, 255, 0.12);
|
| 1726 |
-
border-color: rgba(143, 136, 255, 0.2);
|
| 1727 |
-
color: var(--text-primary);
|
| 1728 |
-
font-weight: 600;
|
| 1729 |
-
box-shadow:
|
| 1730 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.1),
|
| 1731 |
-
0 2px 8px rgba(143, 136, 255, 0.2);
|
| 1732 |
-
}
|
| 1733 |
-
|
| 1734 |
-
button.secondary.active::before {
|
| 1735 |
-
height: 60%;
|
| 1736 |
-
opacity: 1;
|
| 1737 |
-
box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
|
| 1738 |
}
|
| 1739 |
|
| 1740 |
button.ghost {
|
| 1741 |
-
background:
|
| 1742 |
-
border: 1px solid
|
| 1743 |
-
border-radius:
|
| 1744 |
-
padding: 10px
|
| 1745 |
-
color:
|
| 1746 |
-
font-weight: 600;
|
| 1747 |
-
font-family: 'Manrope', sans-serif;
|
| 1748 |
-
font-size: 0.875rem;
|
| 1749 |
cursor: pointer;
|
| 1750 |
-
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1751 |
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
| 1752 |
-
position: relative;
|
| 1753 |
-
overflow: visible;
|
| 1754 |
-
display: flex;
|
| 1755 |
-
align-items: center;
|
| 1756 |
-
gap: 10px;
|
| 1757 |
-
}
|
| 1758 |
-
|
| 1759 |
-
button.ghost::before {
|
| 1760 |
-
content: '';
|
| 1761 |
-
position: absolute;
|
| 1762 |
-
left: 0;
|
| 1763 |
-
top: 50%;
|
| 1764 |
-
transform: translateY(-50%);
|
| 1765 |
-
width: 2px;
|
| 1766 |
-
height: 0;
|
| 1767 |
-
background: var(--primary);
|
| 1768 |
-
border-radius: 0 2px 2px 0;
|
| 1769 |
-
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1770 |
-
opacity: 0;
|
| 1771 |
-
}
|
| 1772 |
-
|
| 1773 |
-
button.ghost::after {
|
| 1774 |
-
content: '';
|
| 1775 |
-
position: absolute;
|
| 1776 |
-
inset: 0;
|
| 1777 |
-
background: rgba(143, 136, 255, 0.05);
|
| 1778 |
-
border-radius: 10px;
|
| 1779 |
-
opacity: 0;
|
| 1780 |
-
transition: opacity 0.25s ease;
|
| 1781 |
-
z-index: -1;
|
| 1782 |
-
}
|
| 1783 |
-
|
| 1784 |
-
button.ghost:hover {
|
| 1785 |
-
background: rgba(255, 255, 255, 1);
|
| 1786 |
-
border-color: rgba(59, 130, 246, 0.3);
|
| 1787 |
-
color: #3b82f6;
|
| 1788 |
-
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
| 1789 |
-
}
|
| 1790 |
-
|
| 1791 |
-
button.ghost:hover::before {
|
| 1792 |
-
height: 50%;
|
| 1793 |
-
opacity: 1;
|
| 1794 |
-
}
|
| 1795 |
-
|
| 1796 |
-
button.ghost:hover::after {
|
| 1797 |
-
opacity: 1;
|
| 1798 |
-
}
|
| 1799 |
-
|
| 1800 |
-
button.ghost.active {
|
| 1801 |
-
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(99, 102, 241, 0.12));
|
| 1802 |
-
border-color: rgba(59, 130, 246, 0.4);
|
| 1803 |
-
color: #3b82f6;
|
| 1804 |
-
box-shadow:
|
| 1805 |
-
inset 0 1px 2px rgba(255, 255, 255, 0.3),
|
| 1806 |
-
0 2px 8px rgba(59, 130, 246, 0.3);
|
| 1807 |
-
}
|
| 1808 |
-
|
| 1809 |
-
button.ghost.active::before {
|
| 1810 |
-
height: 60%;
|
| 1811 |
-
opacity: 1;
|
| 1812 |
-
box-shadow: 0 0 8px rgba(143, 136, 255, 0.5);
|
| 1813 |
}
|
| 1814 |
|
| 1815 |
.skeleton {
|
|
@@ -1900,107 +502,6 @@ button.ghost.active::before {
|
|
| 1900 |
font-size: 0.75rem;
|
| 1901 |
}
|
| 1902 |
|
| 1903 |
-
.backend-info-grid {
|
| 1904 |
-
display: grid;
|
| 1905 |
-
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 1906 |
-
gap: 16px;
|
| 1907 |
-
}
|
| 1908 |
-
|
| 1909 |
-
.backend-info-item {
|
| 1910 |
-
display: flex;
|
| 1911 |
-
flex-direction: column;
|
| 1912 |
-
gap: 8px;
|
| 1913 |
-
padding: 16px;
|
| 1914 |
-
background: rgba(255, 255, 255, 0.95);
|
| 1915 |
-
border: 1px solid rgba(0, 0, 0, 0.08);
|
| 1916 |
-
border-radius: 12px;
|
| 1917 |
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
| 1918 |
-
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
| 1919 |
-
}
|
| 1920 |
-
|
| 1921 |
-
.backend-info-item:hover {
|
| 1922 |
-
background: rgba(255, 255, 255, 1);
|
| 1923 |
-
border-color: rgba(59, 130, 246, 0.3);
|
| 1924 |
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 1925 |
-
}
|
| 1926 |
-
|
| 1927 |
-
.info-label {
|
| 1928 |
-
font-size: 0.75rem;
|
| 1929 |
-
font-weight: 700;
|
| 1930 |
-
color: #64748b;
|
| 1931 |
-
text-transform: uppercase;
|
| 1932 |
-
letter-spacing: 0.08em;
|
| 1933 |
-
}
|
| 1934 |
-
|
| 1935 |
-
.info-value {
|
| 1936 |
-
font-size: 1.125rem;
|
| 1937 |
-
font-weight: 700;
|
| 1938 |
-
color: #0f172a;
|
| 1939 |
-
font-family: 'Manrope', sans-serif;
|
| 1940 |
-
}
|
| 1941 |
-
|
| 1942 |
-
.fear-greed-card {
|
| 1943 |
-
position: relative;
|
| 1944 |
-
overflow: hidden;
|
| 1945 |
-
}
|
| 1946 |
-
|
| 1947 |
-
.fear-greed-card::before {
|
| 1948 |
-
content: '';
|
| 1949 |
-
position: absolute;
|
| 1950 |
-
top: 0;
|
| 1951 |
-
left: 0;
|
| 1952 |
-
right: 0;
|
| 1953 |
-
height: 3px;
|
| 1954 |
-
background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
|
| 1955 |
-
opacity: 0.6;
|
| 1956 |
-
}
|
| 1957 |
-
|
| 1958 |
-
.fear-greed-value {
|
| 1959 |
-
font-size: 2.5rem !important;
|
| 1960 |
-
font-weight: 800 !important;
|
| 1961 |
-
line-height: 1;
|
| 1962 |
-
}
|
| 1963 |
-
|
| 1964 |
-
.fear-greed-classification {
|
| 1965 |
-
font-size: 0.875rem;
|
| 1966 |
-
font-weight: 600;
|
| 1967 |
-
margin-top: 8px;
|
| 1968 |
-
text-transform: uppercase;
|
| 1969 |
-
letter-spacing: 0.05em;
|
| 1970 |
-
}
|
| 1971 |
-
|
| 1972 |
-
.fear-greed-gauge {
|
| 1973 |
-
margin-top: 16px;
|
| 1974 |
-
}
|
| 1975 |
-
|
| 1976 |
-
.gauge-bar {
|
| 1977 |
-
background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%);
|
| 1978 |
-
height: 8px;
|
| 1979 |
-
border-radius: 4px;
|
| 1980 |
-
position: relative;
|
| 1981 |
-
overflow: visible;
|
| 1982 |
-
}
|
| 1983 |
-
|
| 1984 |
-
.gauge-indicator {
|
| 1985 |
-
position: absolute;
|
| 1986 |
-
top: 50%;
|
| 1987 |
-
transform: translate(-50%, -50%);
|
| 1988 |
-
width: 16px;
|
| 1989 |
-
height: 16px;
|
| 1990 |
-
border: 2px solid #fff;
|
| 1991 |
-
border-radius: 50%;
|
| 1992 |
-
box-shadow: 0 0 8px currentColor;
|
| 1993 |
-
transition: left 0.3s ease;
|
| 1994 |
-
}
|
| 1995 |
-
|
| 1996 |
-
.gauge-labels {
|
| 1997 |
-
display: flex;
|
| 1998 |
-
justify-content: space-between;
|
| 1999 |
-
margin-top: 8px;
|
| 2000 |
-
font-size: 0.75rem;
|
| 2001 |
-
color: var(--text-muted);
|
| 2002 |
-
}
|
| 2003 |
-
|
| 2004 |
.toggle {
|
| 2005 |
position: relative;
|
| 2006 |
width: 44px;
|
|
@@ -2040,665 +541,11 @@ button.ghost.active::before {
|
|
| 2040 |
100% { background: transparent; }
|
| 2041 |
}
|
| 2042 |
|
| 2043 |
-
.table-container {
|
| 2044 |
-
overflow-x: auto;
|
| 2045 |
-
border-radius: 16px;
|
| 2046 |
-
background: rgba(255, 255, 255, 0.02);
|
| 2047 |
-
border: 1px solid var(--glass-border);
|
| 2048 |
-
}
|
| 2049 |
-
|
| 2050 |
-
.chip {
|
| 2051 |
-
display: inline-flex;
|
| 2052 |
-
align-items: center;
|
| 2053 |
-
padding: 6px 12px;
|
| 2054 |
-
border-radius: 999px;
|
| 2055 |
-
background: rgba(255, 255, 255, 0.08);
|
| 2056 |
-
font-size: 0.8125rem;
|
| 2057 |
-
font-weight: 500;
|
| 2058 |
-
font-family: 'Inter', sans-serif;
|
| 2059 |
-
color: var(--text-primary);
|
| 2060 |
-
}
|
| 2061 |
-
|
| 2062 |
-
/* Modern Sentiment UI - Professional Design */
|
| 2063 |
-
.sentiment-modern {
|
| 2064 |
-
display: flex;
|
| 2065 |
-
flex-direction: column;
|
| 2066 |
-
gap: 1.75rem;
|
| 2067 |
-
}
|
| 2068 |
-
|
| 2069 |
-
.sentiment-header {
|
| 2070 |
-
display: flex;
|
| 2071 |
-
justify-content: space-between;
|
| 2072 |
-
align-items: center;
|
| 2073 |
-
margin-bottom: 0.75rem;
|
| 2074 |
-
padding-bottom: 1rem;
|
| 2075 |
-
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
| 2076 |
-
}
|
| 2077 |
-
|
| 2078 |
-
.sentiment-header h4 {
|
| 2079 |
-
margin: 0;
|
| 2080 |
-
font-size: 1.25rem;
|
| 2081 |
-
font-weight: 700;
|
| 2082 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2083 |
-
letter-spacing: -0.02em;
|
| 2084 |
-
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
|
| 2085 |
-
-webkit-background-clip: text;
|
| 2086 |
-
-webkit-text-fill-color: transparent;
|
| 2087 |
-
background-clip: text;
|
| 2088 |
-
}
|
| 2089 |
-
|
| 2090 |
-
.sentiment-badge {
|
| 2091 |
-
display: inline-flex;
|
| 2092 |
-
align-items: center;
|
| 2093 |
-
gap: 0.5rem;
|
| 2094 |
-
padding: 6px 14px;
|
| 2095 |
-
border-radius: 999px;
|
| 2096 |
-
background: linear-gradient(135deg, rgba(143, 136, 255, 0.25), rgba(22, 217, 250, 0.25));
|
| 2097 |
-
border: 1px solid rgba(143, 136, 255, 0.4);
|
| 2098 |
-
font-size: 0.75rem;
|
| 2099 |
-
font-weight: 700;
|
| 2100 |
-
text-transform: uppercase;
|
| 2101 |
-
letter-spacing: 0.1em;
|
| 2102 |
-
color: var(--primary-light);
|
| 2103 |
-
box-shadow: 0 4px 12px rgba(143, 136, 255, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 2104 |
-
font-family: 'Manrope', sans-serif;
|
| 2105 |
-
}
|
| 2106 |
-
|
| 2107 |
-
.sentiment-cards {
|
| 2108 |
-
display: flex;
|
| 2109 |
-
flex-direction: column;
|
| 2110 |
-
gap: 1.25rem;
|
| 2111 |
-
}
|
| 2112 |
-
|
| 2113 |
-
.sentiment-item {
|
| 2114 |
-
display: flex;
|
| 2115 |
-
flex-direction: column;
|
| 2116 |
-
gap: 0.75rem;
|
| 2117 |
-
padding: 1.25rem;
|
| 2118 |
-
background: rgba(255, 255, 255, 0.04);
|
| 2119 |
-
border-radius: 16px;
|
| 2120 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 2121 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 2122 |
-
position: relative;
|
| 2123 |
-
overflow: hidden;
|
| 2124 |
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
| 2125 |
-
}
|
| 2126 |
-
|
| 2127 |
-
.sentiment-item::before {
|
| 2128 |
-
content: '';
|
| 2129 |
-
position: absolute;
|
| 2130 |
-
top: 0;
|
| 2131 |
-
left: 0;
|
| 2132 |
-
width: 4px;
|
| 2133 |
-
height: 100%;
|
| 2134 |
-
background: currentColor;
|
| 2135 |
-
opacity: 0.6;
|
| 2136 |
-
transform: scaleY(0);
|
| 2137 |
-
transform-origin: bottom;
|
| 2138 |
-
transition: transform 0.3s ease;
|
| 2139 |
-
}
|
| 2140 |
-
|
| 2141 |
-
.sentiment-item:hover {
|
| 2142 |
-
background: rgba(255, 255, 255, 0.08);
|
| 2143 |
-
border-color: rgba(255, 255, 255, 0.15);
|
| 2144 |
-
transform: translateX(6px) translateY(-2px);
|
| 2145 |
-
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 2146 |
-
}
|
| 2147 |
-
|
| 2148 |
-
.sentiment-item:hover::before {
|
| 2149 |
-
transform: scaleY(1);
|
| 2150 |
-
}
|
| 2151 |
-
|
| 2152 |
-
.sentiment-item-header {
|
| 2153 |
-
display: flex;
|
| 2154 |
-
align-items: center;
|
| 2155 |
-
gap: 0.75rem;
|
| 2156 |
-
}
|
| 2157 |
-
|
| 2158 |
-
.sentiment-icon {
|
| 2159 |
-
display: flex;
|
| 2160 |
-
align-items: center;
|
| 2161 |
-
justify-content: center;
|
| 2162 |
-
width: 40px;
|
| 2163 |
-
height: 40px;
|
| 2164 |
-
border-radius: 12px;
|
| 2165 |
-
flex-shrink: 0;
|
| 2166 |
-
transition: all 0.3s ease;
|
| 2167 |
-
}
|
| 2168 |
-
|
| 2169 |
-
.sentiment-item:hover .sentiment-icon {
|
| 2170 |
-
transform: scale(1.15) rotate(5deg);
|
| 2171 |
-
}
|
| 2172 |
-
|
| 2173 |
-
.sentiment-item.bullish {
|
| 2174 |
-
color: #22c55e;
|
| 2175 |
-
}
|
| 2176 |
-
|
| 2177 |
-
.sentiment-item.bullish .sentiment-icon {
|
| 2178 |
-
background: linear-gradient(135deg, rgba(34, 197, 94, 0.25), rgba(16, 185, 129, 0.2));
|
| 2179 |
-
color: #22c55e;
|
| 2180 |
-
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 2181 |
-
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 2182 |
-
}
|
| 2183 |
-
|
| 2184 |
-
.sentiment-item.neutral {
|
| 2185 |
-
color: #38bdf8;
|
| 2186 |
-
}
|
| 2187 |
-
|
| 2188 |
-
.sentiment-item.neutral .sentiment-icon {
|
| 2189 |
-
background: linear-gradient(135deg, rgba(56, 189, 248, 0.25), rgba(14, 165, 233, 0.2));
|
| 2190 |
-
color: #38bdf8;
|
| 2191 |
-
border: 1px solid rgba(56, 189, 248, 0.3);
|
| 2192 |
-
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 2193 |
-
}
|
| 2194 |
-
|
| 2195 |
-
.sentiment-item.bearish {
|
| 2196 |
-
color: #ef4444;
|
| 2197 |
-
}
|
| 2198 |
-
|
| 2199 |
-
.sentiment-item.bearish .sentiment-icon {
|
| 2200 |
-
background: linear-gradient(135deg, rgba(239, 68, 68, 0.25), rgba(220, 38, 38, 0.2));
|
| 2201 |
-
color: #ef4444;
|
| 2202 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 2203 |
-
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
| 2204 |
-
}
|
| 2205 |
-
|
| 2206 |
-
.sentiment-label {
|
| 2207 |
-
flex: 1;
|
| 2208 |
-
font-size: 1rem;
|
| 2209 |
-
font-weight: 600;
|
| 2210 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2211 |
-
color: var(--text-primary);
|
| 2212 |
-
letter-spacing: -0.01em;
|
| 2213 |
-
}
|
| 2214 |
-
|
| 2215 |
-
.sentiment-percent {
|
| 2216 |
-
font-size: 1.125rem;
|
| 2217 |
-
font-weight: 800;
|
| 2218 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2219 |
-
color: var(--text-primary);
|
| 2220 |
-
letter-spacing: -0.02em;
|
| 2221 |
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 2222 |
-
}
|
| 2223 |
-
|
| 2224 |
-
.sentiment-progress {
|
| 2225 |
-
width: 100%;
|
| 2226 |
-
height: 10px;
|
| 2227 |
-
background: rgba(0, 0, 0, 0.3);
|
| 2228 |
-
border-radius: 999px;
|
| 2229 |
-
overflow: hidden;
|
| 2230 |
-
position: relative;
|
| 2231 |
-
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 2232 |
-
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
|
| 2233 |
-
}
|
| 2234 |
-
|
| 2235 |
-
.sentiment-progress-bar {
|
| 2236 |
-
height: 100%;
|
| 2237 |
-
border-radius: 999px;
|
| 2238 |
-
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
| 2239 |
-
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 2240 |
-
position: relative;
|
| 2241 |
-
overflow: hidden;
|
| 2242 |
-
}
|
| 2243 |
-
|
| 2244 |
-
.sentiment-progress-bar::after {
|
| 2245 |
-
content: '';
|
| 2246 |
-
position: absolute;
|
| 2247 |
-
top: 0;
|
| 2248 |
-
left: 0;
|
| 2249 |
-
right: 0;
|
| 2250 |
-
bottom: 0;
|
| 2251 |
-
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 2252 |
-
animation: shimmer 2s infinite;
|
| 2253 |
-
}
|
| 2254 |
-
|
| 2255 |
-
@keyframes shimmer {
|
| 2256 |
-
0% { transform: translateX(-100%); }
|
| 2257 |
-
100% { transform: translateX(100%); }
|
| 2258 |
-
}
|
| 2259 |
-
|
| 2260 |
-
.sentiment-summary {
|
| 2261 |
-
display: flex;
|
| 2262 |
-
gap: 2rem;
|
| 2263 |
-
padding: 1.25rem;
|
| 2264 |
-
background: rgba(255, 255, 255, 0.03);
|
| 2265 |
-
border-radius: 14px;
|
| 2266 |
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 2267 |
-
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
| 2268 |
-
}
|
| 2269 |
-
|
| 2270 |
-
.sentiment-summary-item {
|
| 2271 |
-
display: flex;
|
| 2272 |
-
flex-direction: column;
|
| 2273 |
-
gap: 0.5rem;
|
| 2274 |
-
flex: 1;
|
| 2275 |
-
}
|
| 2276 |
-
|
| 2277 |
-
.summary-label {
|
| 2278 |
-
font-size: 0.75rem;
|
| 2279 |
-
text-transform: uppercase;
|
| 2280 |
-
letter-spacing: 0.1em;
|
| 2281 |
-
color: var(--text-muted);
|
| 2282 |
-
font-weight: 600;
|
| 2283 |
-
font-family: 'Manrope', sans-serif;
|
| 2284 |
-
}
|
| 2285 |
-
|
| 2286 |
-
.summary-value {
|
| 2287 |
-
font-size: 1.5rem;
|
| 2288 |
-
font-weight: 800;
|
| 2289 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2290 |
-
letter-spacing: -0.02em;
|
| 2291 |
-
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 2292 |
-
}
|
| 2293 |
-
|
| 2294 |
-
.summary-value.bullish {
|
| 2295 |
-
color: #22c55e;
|
| 2296 |
-
text-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
|
| 2297 |
-
}
|
| 2298 |
-
|
| 2299 |
-
.summary-value.neutral {
|
| 2300 |
-
color: #38bdf8;
|
| 2301 |
-
text-shadow: 0 0 20px rgba(56, 189, 248, 0.4);
|
| 2302 |
-
}
|
| 2303 |
-
|
| 2304 |
-
.summary-value.bearish {
|
| 2305 |
-
color: #ef4444;
|
| 2306 |
-
text-shadow: 0 0 20px rgba(239, 68, 68, 0.4);
|
| 2307 |
-
}
|
| 2308 |
-
|
| 2309 |
@keyframes fadeIn {
|
| 2310 |
from { opacity: 0; transform: translateY(8px); }
|
| 2311 |
to { opacity: 1; transform: translateY(0); }
|
| 2312 |
}
|
| 2313 |
|
| 2314 |
-
/* Chart Lab Styles */
|
| 2315 |
-
.chart-controls {
|
| 2316 |
-
display: flex;
|
| 2317 |
-
flex-direction: column;
|
| 2318 |
-
gap: 1.5rem;
|
| 2319 |
-
padding: 1.5rem;
|
| 2320 |
-
background: var(--glass-bg);
|
| 2321 |
-
border: 1px solid var(--glass-border);
|
| 2322 |
-
border-radius: 20px;
|
| 2323 |
-
backdrop-filter: blur(20px);
|
| 2324 |
-
}
|
| 2325 |
-
|
| 2326 |
-
.chart-label {
|
| 2327 |
-
display: block;
|
| 2328 |
-
font-size: 0.8125rem;
|
| 2329 |
-
font-weight: 600;
|
| 2330 |
-
text-transform: uppercase;
|
| 2331 |
-
letter-spacing: 0.05em;
|
| 2332 |
-
color: var(--text-muted);
|
| 2333 |
-
margin-bottom: 0.5rem;
|
| 2334 |
-
font-family: 'Manrope', sans-serif;
|
| 2335 |
-
}
|
| 2336 |
-
|
| 2337 |
-
.chart-symbol-selector {
|
| 2338 |
-
flex: 1;
|
| 2339 |
-
}
|
| 2340 |
-
|
| 2341 |
-
.combobox-wrapper {
|
| 2342 |
-
position: relative;
|
| 2343 |
-
}
|
| 2344 |
-
|
| 2345 |
-
.combobox-input {
|
| 2346 |
-
width: 100%;
|
| 2347 |
-
padding: 12px 16px;
|
| 2348 |
-
background: rgba(255, 255, 255, 0.05);
|
| 2349 |
-
border: 1px solid var(--glass-border);
|
| 2350 |
-
border-radius: 12px;
|
| 2351 |
-
color: var(--text-primary);
|
| 2352 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2353 |
-
font-size: 0.9375rem;
|
| 2354 |
-
transition: all 0.2s ease;
|
| 2355 |
-
}
|
| 2356 |
-
|
| 2357 |
-
.combobox-input:focus {
|
| 2358 |
-
outline: none;
|
| 2359 |
-
border-color: var(--primary);
|
| 2360 |
-
background: rgba(255, 255, 255, 0.08);
|
| 2361 |
-
box-shadow: 0 0 0 3px rgba(143, 136, 255, 0.2);
|
| 2362 |
-
}
|
| 2363 |
-
|
| 2364 |
-
.combobox-dropdown {
|
| 2365 |
-
position: absolute;
|
| 2366 |
-
top: 100%;
|
| 2367 |
-
left: 0;
|
| 2368 |
-
right: 0;
|
| 2369 |
-
margin-top: 0.5rem;
|
| 2370 |
-
background: var(--glass-bg);
|
| 2371 |
-
border: 1px solid var(--glass-border);
|
| 2372 |
-
border-radius: 12px;
|
| 2373 |
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
| 2374 |
-
backdrop-filter: blur(20px);
|
| 2375 |
-
z-index: 100;
|
| 2376 |
-
max-height: 300px;
|
| 2377 |
-
overflow-y: auto;
|
| 2378 |
-
}
|
| 2379 |
-
|
| 2380 |
-
.combobox-options {
|
| 2381 |
-
padding: 0.5rem;
|
| 2382 |
-
}
|
| 2383 |
-
|
| 2384 |
-
.combobox-option {
|
| 2385 |
-
display: flex;
|
| 2386 |
-
justify-content: space-between;
|
| 2387 |
-
align-items: center;
|
| 2388 |
-
padding: 10px 14px;
|
| 2389 |
-
border-radius: 8px;
|
| 2390 |
-
cursor: pointer;
|
| 2391 |
-
transition: all 0.2s ease;
|
| 2392 |
-
font-family: 'Manrope', sans-serif;
|
| 2393 |
-
}
|
| 2394 |
-
|
| 2395 |
-
.combobox-option:hover {
|
| 2396 |
-
background: rgba(255, 255, 255, 0.1);
|
| 2397 |
-
transform: translateX(4px);
|
| 2398 |
-
}
|
| 2399 |
-
|
| 2400 |
-
.combobox-option.disabled {
|
| 2401 |
-
opacity: 0.5;
|
| 2402 |
-
cursor: not-allowed;
|
| 2403 |
-
}
|
| 2404 |
-
|
| 2405 |
-
.combobox-option strong {
|
| 2406 |
-
font-weight: 700;
|
| 2407 |
-
color: var(--text-primary);
|
| 2408 |
-
font-size: 0.9375rem;
|
| 2409 |
-
}
|
| 2410 |
-
|
| 2411 |
-
.combobox-option span {
|
| 2412 |
-
color: var(--text-muted);
|
| 2413 |
-
font-size: 0.875rem;
|
| 2414 |
-
}
|
| 2415 |
-
|
| 2416 |
-
.chart-timeframe-selector {
|
| 2417 |
-
display: flex;
|
| 2418 |
-
flex-direction: column;
|
| 2419 |
-
gap: 0.5rem;
|
| 2420 |
-
}
|
| 2421 |
-
|
| 2422 |
-
.chart-actions {
|
| 2423 |
-
display: flex;
|
| 2424 |
-
align-items: flex-end;
|
| 2425 |
-
}
|
| 2426 |
-
|
| 2427 |
-
.chart-container {
|
| 2428 |
-
padding: 1.5rem;
|
| 2429 |
-
background: rgba(0, 0, 0, 0.15);
|
| 2430 |
-
border-radius: 16px;
|
| 2431 |
-
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 2432 |
-
}
|
| 2433 |
-
|
| 2434 |
-
.chart-header {
|
| 2435 |
-
display: flex;
|
| 2436 |
-
justify-content: space-between;
|
| 2437 |
-
align-items: center;
|
| 2438 |
-
margin-bottom: 1.5rem;
|
| 2439 |
-
padding-bottom: 1rem;
|
| 2440 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 2441 |
-
}
|
| 2442 |
-
|
| 2443 |
-
.chart-header h4 {
|
| 2444 |
-
margin: 0;
|
| 2445 |
-
}
|
| 2446 |
-
|
| 2447 |
-
.chart-legend {
|
| 2448 |
-
display: flex;
|
| 2449 |
-
gap: 2rem;
|
| 2450 |
-
flex-wrap: wrap;
|
| 2451 |
-
}
|
| 2452 |
-
|
| 2453 |
-
.legend-item {
|
| 2454 |
-
display: flex;
|
| 2455 |
-
flex-direction: column;
|
| 2456 |
-
gap: 0.25rem;
|
| 2457 |
-
}
|
| 2458 |
-
|
| 2459 |
-
.legend-label {
|
| 2460 |
-
font-size: 0.7rem;
|
| 2461 |
-
text-transform: uppercase;
|
| 2462 |
-
letter-spacing: 0.08em;
|
| 2463 |
-
color: rgba(226, 232, 240, 0.5);
|
| 2464 |
-
font-weight: 600;
|
| 2465 |
-
font-family: 'Manrope', sans-serif;
|
| 2466 |
-
}
|
| 2467 |
-
|
| 2468 |
-
.legend-value {
|
| 2469 |
-
font-size: 1rem;
|
| 2470 |
-
font-weight: 600;
|
| 2471 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2472 |
-
color: var(--text-primary);
|
| 2473 |
-
display: flex;
|
| 2474 |
-
align-items: center;
|
| 2475 |
-
gap: 0.25rem;
|
| 2476 |
-
}
|
| 2477 |
-
|
| 2478 |
-
.legend-arrow {
|
| 2479 |
-
font-size: 0.875rem;
|
| 2480 |
-
opacity: 0.8;
|
| 2481 |
-
}
|
| 2482 |
-
|
| 2483 |
-
.legend-value.positive {
|
| 2484 |
-
color: #26a69a;
|
| 2485 |
-
}
|
| 2486 |
-
|
| 2487 |
-
.legend-value.negative {
|
| 2488 |
-
color: #ef5350;
|
| 2489 |
-
}
|
| 2490 |
-
|
| 2491 |
-
.chart-wrapper {
|
| 2492 |
-
position: relative;
|
| 2493 |
-
height: 450px;
|
| 2494 |
-
padding: 0;
|
| 2495 |
-
background: rgba(0, 0, 0, 0.2);
|
| 2496 |
-
border-radius: 12px;
|
| 2497 |
-
overflow: hidden;
|
| 2498 |
-
}
|
| 2499 |
-
|
| 2500 |
-
.chart-wrapper canvas {
|
| 2501 |
-
padding: 12px;
|
| 2502 |
-
}
|
| 2503 |
-
|
| 2504 |
-
.chart-loading {
|
| 2505 |
-
position: absolute;
|
| 2506 |
-
inset: 0;
|
| 2507 |
-
display: flex;
|
| 2508 |
-
flex-direction: column;
|
| 2509 |
-
align-items: center;
|
| 2510 |
-
justify-content: center;
|
| 2511 |
-
gap: 1rem;
|
| 2512 |
-
background: rgba(0, 0, 0, 0.3);
|
| 2513 |
-
border-radius: 12px;
|
| 2514 |
-
z-index: 10;
|
| 2515 |
-
}
|
| 2516 |
-
|
| 2517 |
-
.loading-spinner {
|
| 2518 |
-
width: 40px;
|
| 2519 |
-
height: 40px;
|
| 2520 |
-
border: 3px solid rgba(255, 255, 255, 0.1);
|
| 2521 |
-
border-top-color: var(--primary);
|
| 2522 |
-
border-radius: 50%;
|
| 2523 |
-
animation: spin 1s linear infinite;
|
| 2524 |
-
}
|
| 2525 |
-
|
| 2526 |
-
@keyframes spin {
|
| 2527 |
-
to { transform: rotate(360deg); }
|
| 2528 |
-
}
|
| 2529 |
-
|
| 2530 |
-
.indicator-selector {
|
| 2531 |
-
margin-top: 1rem;
|
| 2532 |
-
}
|
| 2533 |
-
|
| 2534 |
-
.indicator-selector .button-group {
|
| 2535 |
-
display: grid;
|
| 2536 |
-
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
| 2537 |
-
gap: 0.75rem;
|
| 2538 |
-
}
|
| 2539 |
-
|
| 2540 |
-
.indicator-selector button {
|
| 2541 |
-
display: flex;
|
| 2542 |
-
flex-direction: column;
|
| 2543 |
-
align-items: center;
|
| 2544 |
-
gap: 0.25rem;
|
| 2545 |
-
padding: 12px 16px;
|
| 2546 |
-
}
|
| 2547 |
-
|
| 2548 |
-
.indicator-selector button span {
|
| 2549 |
-
font-weight: 600;
|
| 2550 |
-
font-size: 0.9375rem;
|
| 2551 |
-
}
|
| 2552 |
-
|
| 2553 |
-
.indicator-selector button small {
|
| 2554 |
-
font-size: 0.75rem;
|
| 2555 |
-
opacity: 0.7;
|
| 2556 |
-
font-weight: 400;
|
| 2557 |
-
}
|
| 2558 |
-
|
| 2559 |
-
.analysis-output {
|
| 2560 |
-
margin-top: 1.5rem;
|
| 2561 |
-
padding-top: 1.5rem;
|
| 2562 |
-
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 2563 |
-
}
|
| 2564 |
-
|
| 2565 |
-
.analysis-loading {
|
| 2566 |
-
display: flex;
|
| 2567 |
-
flex-direction: column;
|
| 2568 |
-
align-items: center;
|
| 2569 |
-
justify-content: center;
|
| 2570 |
-
gap: 1rem;
|
| 2571 |
-
padding: 2rem;
|
| 2572 |
-
}
|
| 2573 |
-
|
| 2574 |
-
.analysis-results {
|
| 2575 |
-
display: flex;
|
| 2576 |
-
flex-direction: column;
|
| 2577 |
-
gap: 1.5rem;
|
| 2578 |
-
}
|
| 2579 |
-
|
| 2580 |
-
.analysis-header {
|
| 2581 |
-
display: flex;
|
| 2582 |
-
justify-content: space-between;
|
| 2583 |
-
align-items: center;
|
| 2584 |
-
padding-bottom: 1rem;
|
| 2585 |
-
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 2586 |
-
}
|
| 2587 |
-
|
| 2588 |
-
.analysis-header h5 {
|
| 2589 |
-
margin: 0;
|
| 2590 |
-
font-size: 1.125rem;
|
| 2591 |
-
font-weight: 700;
|
| 2592 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2593 |
-
}
|
| 2594 |
-
|
| 2595 |
-
.analysis-badge {
|
| 2596 |
-
padding: 4px 12px;
|
| 2597 |
-
border-radius: 999px;
|
| 2598 |
-
font-size: 0.75rem;
|
| 2599 |
-
font-weight: 700;
|
| 2600 |
-
text-transform: uppercase;
|
| 2601 |
-
letter-spacing: 0.05em;
|
| 2602 |
-
}
|
| 2603 |
-
|
| 2604 |
-
.analysis-badge.bullish {
|
| 2605 |
-
background: rgba(34, 197, 94, 0.2);
|
| 2606 |
-
color: var(--success);
|
| 2607 |
-
border: 1px solid rgba(34, 197, 94, 0.3);
|
| 2608 |
-
}
|
| 2609 |
-
|
| 2610 |
-
.analysis-badge.bearish {
|
| 2611 |
-
background: rgba(239, 68, 68, 0.2);
|
| 2612 |
-
color: var(--danger);
|
| 2613 |
-
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 2614 |
-
}
|
| 2615 |
-
|
| 2616 |
-
.analysis-badge.neutral {
|
| 2617 |
-
background: rgba(56, 189, 248, 0.2);
|
| 2618 |
-
color: var(--info);
|
| 2619 |
-
border: 1px solid rgba(56, 189, 248, 0.3);
|
| 2620 |
-
}
|
| 2621 |
-
|
| 2622 |
-
.analysis-metrics {
|
| 2623 |
-
display: grid;
|
| 2624 |
-
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
| 2625 |
-
gap: 1rem;
|
| 2626 |
-
}
|
| 2627 |
-
|
| 2628 |
-
.metric-item {
|
| 2629 |
-
display: flex;
|
| 2630 |
-
flex-direction: column;
|
| 2631 |
-
gap: 0.5rem;
|
| 2632 |
-
padding: 1rem;
|
| 2633 |
-
background: rgba(255, 255, 255, 0.03);
|
| 2634 |
-
border-radius: 12px;
|
| 2635 |
-
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 2636 |
-
}
|
| 2637 |
-
|
| 2638 |
-
.metric-label {
|
| 2639 |
-
font-size: 0.75rem;
|
| 2640 |
-
text-transform: uppercase;
|
| 2641 |
-
letter-spacing: 0.05em;
|
| 2642 |
-
color: var(--text-muted);
|
| 2643 |
-
font-weight: 600;
|
| 2644 |
-
}
|
| 2645 |
-
|
| 2646 |
-
.metric-value {
|
| 2647 |
-
font-size: 1.25rem;
|
| 2648 |
-
font-weight: 700;
|
| 2649 |
-
font-family: 'Manrope', 'DM Sans', sans-serif;
|
| 2650 |
-
color: var(--text-primary);
|
| 2651 |
-
}
|
| 2652 |
-
|
| 2653 |
-
.metric-value.positive {
|
| 2654 |
-
color: var(--success);
|
| 2655 |
-
}
|
| 2656 |
-
|
| 2657 |
-
.metric-value.negative {
|
| 2658 |
-
color: var(--danger);
|
| 2659 |
-
}
|
| 2660 |
-
|
| 2661 |
-
.analysis-summary,
|
| 2662 |
-
.analysis-signals {
|
| 2663 |
-
padding: 1rem;
|
| 2664 |
-
background: rgba(255, 255, 255, 0.03);
|
| 2665 |
-
border-radius: 12px;
|
| 2666 |
-
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 2667 |
-
}
|
| 2668 |
-
|
| 2669 |
-
.analysis-summary h6,
|
| 2670 |
-
.analysis-signals h6 {
|
| 2671 |
-
margin: 0 0 0.75rem 0;
|
| 2672 |
-
font-size: 0.9375rem;
|
| 2673 |
-
font-weight: 600;
|
| 2674 |
-
text-transform: uppercase;
|
| 2675 |
-
letter-spacing: 0.05em;
|
| 2676 |
-
color: var(--text-muted);
|
| 2677 |
-
}
|
| 2678 |
-
|
| 2679 |
-
.analysis-summary p {
|
| 2680 |
-
margin: 0;
|
| 2681 |
-
line-height: 1.6;
|
| 2682 |
-
color: var(--text-secondary);
|
| 2683 |
-
}
|
| 2684 |
-
|
| 2685 |
-
.analysis-signals ul {
|
| 2686 |
-
margin: 0;
|
| 2687 |
-
padding-left: 1.5rem;
|
| 2688 |
-
list-style: disc;
|
| 2689 |
-
}
|
| 2690 |
-
|
| 2691 |
-
.analysis-signals li {
|
| 2692 |
-
margin-bottom: 0.5rem;
|
| 2693 |
-
color: var(--text-secondary);
|
| 2694 |
-
line-height: 1.6;
|
| 2695 |
-
}
|
| 2696 |
-
|
| 2697 |
-
.analysis-signals li strong {
|
| 2698 |
-
color: var(--text-primary);
|
| 2699 |
-
font-weight: 600;
|
| 2700 |
-
}
|
| 2701 |
-
|
| 2702 |
@keyframes shimmer {
|
| 2703 |
100% { transform: translateX(100%); }
|
| 2704 |
}
|
|
|
|
| 1 |
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
| 2 |
|
| 3 |
:root {
|
| 4 |
+
--bg-gradient: radial-gradient(circle at top, #172032, #05060a 60%);
|
| 5 |
+
--glass-bg: rgba(17, 25, 40, 0.65);
|
| 6 |
+
--glass-border: rgba(255, 255, 255, 0.08);
|
| 7 |
+
--glass-highlight: rgba(255, 255, 255, 0.15);
|
| 8 |
--primary: #8f88ff;
|
| 9 |
--primary-strong: #6c63ff;
|
|
|
|
|
|
|
| 10 |
--secondary: #16d9fa;
|
|
|
|
|
|
|
| 11 |
--accent: #f472b6;
|
| 12 |
+
--success: #22c55e;
|
|
|
|
|
|
|
|
|
|
| 13 |
--warning: #facc15;
|
|
|
|
| 14 |
--danger: #ef4444;
|
|
|
|
| 15 |
--info: #38bdf8;
|
| 16 |
+
--text-primary: #f8fafc;
|
| 17 |
+
--text-muted: rgba(248, 250, 252, 0.7);
|
| 18 |
+
--shadow-strong: 0 25px 60px rgba(0, 0, 0, 0.45);
|
| 19 |
+
--shadow-soft: 0 15px 40px rgba(0, 0, 0, 0.35);
|
|
|
|
|
|
|
|
|
|
| 20 |
--sidebar-width: 260px;
|
| 21 |
}
|
| 22 |
|
|
|
|
| 28 |
margin: 0;
|
| 29 |
padding: 0;
|
| 30 |
min-height: 100vh;
|
| 31 |
+
font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
background: var(--bg-gradient);
|
| 33 |
color: var(--text-primary);
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
body[data-theme='light'] {
|
|
|
|
| 49 |
|
| 50 |
.sidebar {
|
| 51 |
width: var(--sidebar-width);
|
| 52 |
+
padding: 32px 24px;
|
| 53 |
+
background: linear-gradient(180deg, rgba(9, 9, 13, 0.8), rgba(9, 9, 13, 0.4));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
backdrop-filter: blur(30px);
|
| 55 |
+
border-right: 1px solid var(--glass-border);
|
| 56 |
display: flex;
|
| 57 |
flex-direction: column;
|
| 58 |
+
gap: 24px;
|
| 59 |
position: sticky;
|
| 60 |
top: 0;
|
| 61 |
height: 100vh;
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
.brand {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
display: flex;
|
| 66 |
flex-direction: column;
|
| 67 |
gap: 6px;
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
.brand strong {
|
| 71 |
+
font-size: 1.3rem;
|
| 72 |
+
letter-spacing: 0.1em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
|
| 75 |
.env-pill {
|
| 76 |
display: inline-flex;
|
| 77 |
align-items: center;
|
| 78 |
+
gap: 6px;
|
| 79 |
+
background: rgba(255, 255, 255, 0.08);
|
| 80 |
+
padding: 4px 10px;
|
| 81 |
+
border-radius: 999px;
|
| 82 |
+
font-size: 0.75rem;
|
|
|
|
|
|
|
| 83 |
text-transform: uppercase;
|
| 84 |
+
letter-spacing: 0.05em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
.nav {
|
|
|
|
| 92 |
|
| 93 |
.nav-button {
|
| 94 |
border: none;
|
| 95 |
+
border-radius: 14px;
|
| 96 |
padding: 12px 16px;
|
| 97 |
display: flex;
|
| 98 |
align-items: center;
|
| 99 |
gap: 12px;
|
| 100 |
background: transparent;
|
| 101 |
+
color: inherit;
|
| 102 |
+
font-weight: 500;
|
|
|
|
|
|
|
| 103 |
cursor: pointer;
|
| 104 |
+
transition: transform 0.3s ease, background 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
|
| 107 |
.nav-button svg {
|
| 108 |
+
width: 22px;
|
| 109 |
+
height: 22px;
|
| 110 |
fill: currentColor;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
+
.nav-button.active,
|
| 114 |
.nav-button:hover {
|
| 115 |
+
background: rgba(255, 255, 255, 0.08);
|
| 116 |
+
transform: translateX(6px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
|
| 119 |
.sidebar-footer {
|
| 120 |
margin-top: auto;
|
| 121 |
+
font-size: 0.85rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
color: var(--text-muted);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
.main-area {
|
|
|
|
| 134 |
display: flex;
|
| 135 |
justify-content: space-between;
|
| 136 |
align-items: center;
|
| 137 |
+
padding: 18px 24px;
|
| 138 |
border-radius: 24px;
|
| 139 |
+
background: var(--glass-bg);
|
| 140 |
+
border: 1px solid var(--glass-border);
|
| 141 |
+
box-shadow: var(--shadow-soft);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
backdrop-filter: blur(20px);
|
| 143 |
flex-wrap: wrap;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
gap: 16px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
.topbar h1 {
|
| 148 |
margin: 0;
|
| 149 |
+
font-size: 1.8rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
.status-group {
|
|
|
|
| 158 |
.status-pill {
|
| 159 |
display: flex;
|
| 160 |
align-items: center;
|
| 161 |
+
gap: 8px;
|
| 162 |
+
padding: 8px 14px;
|
| 163 |
+
border-radius: 999px;
|
| 164 |
+
background: rgba(255, 255, 255, 0.05);
|
| 165 |
+
border: 1px solid var(--glass-border);
|
| 166 |
+
font-size: 0.85rem;
|
|
|
|
| 167 |
text-transform: uppercase;
|
| 168 |
+
letter-spacing: 0.05em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
.status-dot {
|
| 172 |
+
width: 10px;
|
| 173 |
+
height: 10px;
|
| 174 |
border-radius: 50%;
|
| 175 |
background: var(--warning);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
|
| 178 |
.status-pill[data-state='ok'] .status-dot {
|
| 179 |
+
background: var(--success);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
|
| 182 |
.status-pill[data-state='warn'] .status-dot {
|
| 183 |
+
background: var(--warning);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
.status-pill[data-state='error'] .status-dot {
|
| 187 |
+
background: var(--danger);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
.page-container {
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
.section-title {
|
| 211 |
+
font-size: 1.3rem;
|
| 212 |
+
letter-spacing: 0.05em;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
.glass-card {
|
| 216 |
+
background: var(--glass-bg);
|
| 217 |
+
border: 1px solid var(--glass-border);
|
|
|
|
|
|
|
| 218 |
border-radius: 24px;
|
| 219 |
+
padding: 20px;
|
| 220 |
+
box-shadow: var(--shadow-strong);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
position: relative;
|
| 222 |
+
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
}
|
| 224 |
|
| 225 |
.glass-card::before {
|
|
|
|
| 242 |
margin-bottom: 24px;
|
| 243 |
}
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
.stat-card h3 {
|
| 246 |
+
font-size: 0.9rem;
|
|
|
|
| 247 |
text-transform: uppercase;
|
| 248 |
+
letter-spacing: 0.08em;
|
| 249 |
+
color: var(--text-muted);
|
|
|
|
|
|
|
|
|
|
| 250 |
}
|
| 251 |
|
| 252 |
.stat-value {
|
| 253 |
+
font-size: 1.9rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
font-weight: 600;
|
| 255 |
+
margin: 12px 0 6px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
}
|
| 257 |
|
| 258 |
.stat-trend {
|
| 259 |
display: flex;
|
| 260 |
align-items: center;
|
| 261 |
gap: 6px;
|
| 262 |
+
font-size: 0.85rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
}
|
| 264 |
|
| 265 |
.grid-two {
|
|
|
|
| 268 |
gap: 20px;
|
| 269 |
}
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
.table-wrapper {
|
| 272 |
overflow: auto;
|
| 273 |
}
|
| 274 |
|
| 275 |
table {
|
| 276 |
width: 100%;
|
| 277 |
+
border-collapse: collapse;
|
|
|
|
| 278 |
}
|
| 279 |
|
| 280 |
th, td {
|
| 281 |
text-align: left;
|
| 282 |
+
padding: 12px 10px;
|
| 283 |
+
font-size: 0.92rem;
|
|
|
|
| 284 |
}
|
| 285 |
|
| 286 |
th {
|
| 287 |
+
font-size: 0.8rem;
|
| 288 |
+
letter-spacing: 0.05em;
|
|
|
|
| 289 |
color: var(--text-muted);
|
| 290 |
text-transform: uppercase;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
}
|
| 292 |
|
| 293 |
tr {
|
| 294 |
+
transition: background 0.3s ease, transform 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
}
|
| 296 |
|
| 297 |
tbody tr:hover {
|
| 298 |
+
background: rgba(255, 255, 255, 0.04);
|
| 299 |
+
transform: translateY(-1px);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
.badge {
|
|
|
|
| 367 |
|
| 368 |
.input-chip {
|
| 369 |
border: 1px solid var(--glass-border);
|
| 370 |
+
background: rgba(255, 255, 255, 0.03);
|
| 371 |
border-radius: 999px;
|
| 372 |
padding: 8px 14px;
|
| 373 |
color: var(--text-muted);
|
| 374 |
display: inline-flex;
|
| 375 |
align-items: center;
|
| 376 |
gap: 10px;
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
|
| 379 |
+
input[type='text'], select, textarea {
|
| 380 |
+
width: 100%;
|
| 381 |
+
background: rgba(255, 255, 255, 0.02);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
border: 1px solid var(--glass-border);
|
| 383 |
+
border-radius: 14px;
|
| 384 |
+
padding: 12px 14px;
|
| 385 |
+
color: var(--text-primary);
|
| 386 |
+
font-family: inherit;
|
| 387 |
}
|
| 388 |
|
| 389 |
+
textarea {
|
| 390 |
+
min-height: 100px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
}
|
| 392 |
|
| 393 |
button.primary {
|
| 394 |
background: linear-gradient(120deg, var(--primary), var(--secondary));
|
| 395 |
border: none;
|
| 396 |
+
border-radius: 999px;
|
| 397 |
color: #fff;
|
| 398 |
+
padding: 12px 24px;
|
| 399 |
+
font-weight: 600;
|
|
|
|
|
|
|
| 400 |
cursor: pointer;
|
| 401 |
+
transition: transform 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
}
|
| 403 |
|
| 404 |
button.primary:hover {
|
| 405 |
+
transform: translateY(-2px) scale(1.01);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
}
|
| 407 |
|
| 408 |
button.ghost {
|
| 409 |
+
background: transparent;
|
| 410 |
+
border: 1px solid var(--glass-border);
|
| 411 |
+
border-radius: 999px;
|
| 412 |
+
padding: 10px 20px;
|
| 413 |
+
color: inherit;
|
|
|
|
|
|
|
|
|
|
| 414 |
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
}
|
| 416 |
|
| 417 |
.skeleton {
|
|
|
|
| 502 |
font-size: 0.75rem;
|
| 503 |
}
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
.toggle {
|
| 506 |
position: relative;
|
| 507 |
width: 44px;
|
|
|
|
| 541 |
100% { background: transparent; }
|
| 542 |
}
|
| 543 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
@keyframes fadeIn {
|
| 545 |
from { opacity: 0; transform: translateY(8px); }
|
| 546 |
to { opacity: 1; transform: translateY(0); }
|
| 547 |
}
|
| 548 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
@keyframes shimmer {
|
| 550 |
100% { transform: translateX(100%); }
|
| 551 |
}
|
static/js/aiAdvisorView.js
CHANGED
|
@@ -1,94 +1,129 @@
|
|
| 1 |
import apiClient from './apiClient.js';
|
|
|
|
| 2 |
|
| 3 |
class AIAdvisorView {
|
| 4 |
constructor(section) {
|
| 5 |
this.section = section;
|
| 6 |
-
this.
|
| 7 |
-
this.
|
| 8 |
-
this.
|
| 9 |
-
this.
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
init() {
|
| 13 |
-
if (this.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
}
|
| 20 |
-
if (this.sentimentForm) {
|
| 21 |
-
this.sentimentForm.addEventListener('submit', async (event) => {
|
| 22 |
-
event.preventDefault();
|
| 23 |
-
const formData = new FormData(this.sentimentForm);
|
| 24 |
-
await this.handleSentiment(formData);
|
| 25 |
-
});
|
| 26 |
-
}
|
| 27 |
}
|
| 28 |
|
| 29 |
-
async
|
| 30 |
-
const
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
if (!result.ok) {
|
| 39 |
-
if (this.queryOutput) {
|
| 40 |
-
this.queryOutput.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
|
| 41 |
-
}
|
| 42 |
-
return;
|
| 43 |
}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
`;
|
| 56 |
}
|
| 57 |
-
}
|
| 58 |
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
if (!
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
this.
|
| 65 |
}
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 71 |
}
|
| 72 |
-
return;
|
| 73 |
}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
const
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
</div>
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
}
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
export default AIAdvisorView;
|
|
|
|
| 1 |
import apiClient from './apiClient.js';
|
| 2 |
+
import { formatCurrency, formatPercent } from './uiUtils.js';
|
| 3 |
|
| 4 |
class AIAdvisorView {
|
| 5 |
constructor(section) {
|
| 6 |
this.section = section;
|
| 7 |
+
this.form = section?.querySelector('[data-ai-form]');
|
| 8 |
+
this.decisionContainer = section?.querySelector('[data-ai-result]');
|
| 9 |
+
this.sentimentContainer = section?.querySelector('[data-sentiment-result]');
|
| 10 |
+
this.disclaimer = section?.querySelector('[data-ai-disclaimer]');
|
| 11 |
+
this.contextInput = section?.querySelector('textarea[name="context"]');
|
| 12 |
+
this.modelSelect = section?.querySelector('select[name="model"]');
|
| 13 |
}
|
| 14 |
|
| 15 |
init() {
|
| 16 |
+
if (!this.form) return;
|
| 17 |
+
this.form.addEventListener('submit', async (event) => {
|
| 18 |
+
event.preventDefault();
|
| 19 |
+
const formData = new FormData(this.form);
|
| 20 |
+
await this.handleSubmit(formData);
|
| 21 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
+
async handleSubmit(formData) {
|
| 25 |
+
const symbol = formData.get('symbol') || 'BTC';
|
| 26 |
+
const horizon = formData.get('horizon') || 'swing';
|
| 27 |
+
const risk = formData.get('risk') || 'moderate';
|
| 28 |
+
const context = (formData.get('context') || '').trim();
|
| 29 |
+
const mode = formData.get('model') || 'auto';
|
| 30 |
+
|
| 31 |
+
if (this.decisionContainer) {
|
| 32 |
+
this.decisionContainer.innerHTML = '<p>Generating AI strategy...</p>';
|
| 33 |
}
|
| 34 |
+
if (this.sentimentContainer && context) {
|
| 35 |
+
this.sentimentContainer.innerHTML = '<p>Running sentiment model...</p>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
+
|
| 38 |
+
const decisionPayload = {
|
| 39 |
+
query: `Provide ${horizon} outlook for ${symbol} with ${risk} risk. ${context}`,
|
| 40 |
+
symbol,
|
| 41 |
+
task: 'decision',
|
| 42 |
+
options: { horizon, risk },
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const jobs = [apiClient.runQuery(decisionPayload)];
|
| 46 |
+
if (context) {
|
| 47 |
+
jobs.push(apiClient.analyzeSentiment({ text: context, mode }));
|
|
|
|
| 48 |
}
|
|
|
|
| 49 |
|
| 50 |
+
const [decisionResult, sentimentResult] = await Promise.all(jobs);
|
| 51 |
+
|
| 52 |
+
if (!decisionResult.ok) {
|
| 53 |
+
this.decisionContainer.innerHTML = `<div class="inline-message inline-error">${decisionResult.error}</div>`;
|
| 54 |
+
} else {
|
| 55 |
+
this.renderDecisionResult(decisionResult.data || {});
|
| 56 |
}
|
| 57 |
+
|
| 58 |
+
if (context && this.sentimentContainer) {
|
| 59 |
+
if (!sentimentResult?.ok) {
|
| 60 |
+
this.sentimentContainer.innerHTML = `<div class="inline-message inline-error">${sentimentResult?.error || 'AI sentiment endpoint unavailable'}</div>`;
|
| 61 |
+
} else {
|
| 62 |
+
this.renderSentimentResult(sentimentResult.data || sentimentResult);
|
| 63 |
}
|
|
|
|
| 64 |
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
renderDecisionResult(response) {
|
| 68 |
+
if (!this.decisionContainer) return;
|
| 69 |
+
const payload = response.data || {};
|
| 70 |
+
const analysis = payload.analysis || payload;
|
| 71 |
+
const summary = analysis.summary?.summary || analysis.summary || 'No summary provided.';
|
| 72 |
+
const signals = analysis.signals || {};
|
| 73 |
+
const topCoins = (payload.top_coins || []).slice(0, 3);
|
| 74 |
+
|
| 75 |
+
this.decisionContainer.innerHTML = `
|
| 76 |
+
<div class="ai-result">
|
| 77 |
+
<p class="text-muted">${response.message || 'Decision support summary'}</p>
|
| 78 |
+
<p>${summary}</p>
|
| 79 |
+
<div class="grid-two">
|
| 80 |
+
<div>
|
| 81 |
+
<h4>Market Signals</h4>
|
| 82 |
+
<ul>
|
| 83 |
+
${Object.entries(signals)
|
| 84 |
+
.map(([, value]) => `<li>${value?.label || 'neutral'} (${value?.score ?? '—'})</li>`)
|
| 85 |
+
.join('') || '<li>No model signals.</li>'}
|
| 86 |
+
</ul>
|
| 87 |
+
</div>
|
| 88 |
+
<div>
|
| 89 |
+
<h4>Watchlist</h4>
|
| 90 |
+
<ul>
|
| 91 |
+
${topCoins
|
| 92 |
+
.map(
|
| 93 |
+
(coin) =>
|
| 94 |
+
`<li>${coin.symbol || coin.ticker}: ${formatCurrency(coin.price)} (${formatPercent(coin.change_24h)})</li>`,
|
| 95 |
+
)
|
| 96 |
+
.join('') || '<li>No coin highlights.</li>'}
|
| 97 |
+
</ul>
|
| 98 |
+
</div>
|
| 99 |
</div>
|
| 100 |
+
</div>
|
| 101 |
+
`;
|
| 102 |
+
if (this.disclaimer) {
|
| 103 |
+
this.disclaimer.textContent =
|
| 104 |
+
response.data?.disclaimer || 'This AI output is experimental research and not financial advice.';
|
| 105 |
}
|
| 106 |
}
|
| 107 |
|
| 108 |
+
renderSentimentResult(result) {
|
| 109 |
+
const container = this.sentimentContainer;
|
| 110 |
+
if (!container) return;
|
| 111 |
+
const payload = result.result || result;
|
| 112 |
+
const signals = result.signals || payload.signals || {};
|
| 113 |
+
container.innerHTML = `
|
| 114 |
+
<div class="glass-card">
|
| 115 |
+
<h4>Sentiment (${result.mode || 'auto'})</h4>
|
| 116 |
+
<p><strong>Label:</strong> ${payload.label || payload.classification || 'neutral'}</p>
|
| 117 |
+
<p><strong>Score:</strong> ${payload.score ?? payload.sentiment?.score ?? '—'}</p>
|
| 118 |
+
<div class="chip-row">
|
| 119 |
+
${Object.entries(signals)
|
| 120 |
+
.map(([key, value]) => `<span class="chip">${key}: ${value?.label || 'n/a'}</span>`)
|
| 121 |
+
.join('') || ''}
|
| 122 |
+
</div>
|
| 123 |
+
<p>${payload.summary?.summary || payload.summary?.summary_text || payload.summary || ''}</p>
|
| 124 |
+
</div>
|
| 125 |
+
`;
|
| 126 |
+
}
|
| 127 |
}
|
| 128 |
|
| 129 |
export default AIAdvisorView;
|
static/js/apiClient.js
CHANGED
|
@@ -2,16 +2,8 @@ const DEFAULT_TTL = 60 * 1000; // 1 minute cache
|
|
| 2 |
|
| 3 |
class ApiClient {
|
| 4 |
constructor() {
|
| 5 |
-
|
| 6 |
-
this.baseURL =
|
| 7 |
-
|
| 8 |
-
// اگر بعداً بخوای دستی عوض کنی، از HTML استفاده کن
|
| 9 |
-
if (typeof window.BACKEND_URL === 'string' && window.BACKEND_URL.trim()) {
|
| 10 |
-
this.baseURL = window.BACKEND_URL.trim().replace(/\/$/, '');
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
console.log('[ApiClient] Using Backend:', this.baseURL);
|
| 14 |
-
|
| 15 |
this.cache = new Map();
|
| 16 |
this.requestLogs = [];
|
| 17 |
this.errorLogs = [];
|
|
@@ -136,66 +128,24 @@ class ApiClient {
|
|
| 136 |
}
|
| 137 |
|
| 138 |
// ===== Specific API helpers =====
|
| 139 |
-
// Note: Backend uses api_server_extended.py which has different endpoints
|
| 140 |
-
|
| 141 |
getHealth() {
|
| 142 |
-
|
| 143 |
-
return this.get('/api/status');
|
| 144 |
}
|
| 145 |
|
| 146 |
getTopCoins(limit = 10) {
|
| 147 |
-
|
| 148 |
-
return this.get('/api/market').then(result => {
|
| 149 |
-
if (result.ok && result.data && result.data.cryptocurrencies) {
|
| 150 |
-
return {
|
| 151 |
-
ok: true,
|
| 152 |
-
data: result.data.cryptocurrencies.slice(0, limit)
|
| 153 |
-
};
|
| 154 |
-
}
|
| 155 |
-
return result;
|
| 156 |
-
});
|
| 157 |
}
|
| 158 |
|
| 159 |
getCoinDetails(symbol) {
|
| 160 |
-
|
| 161 |
-
return this.get('/api/market').then(result => {
|
| 162 |
-
if (result.ok && result.data && result.data.cryptocurrencies) {
|
| 163 |
-
const coin = result.data.cryptocurrencies.find(
|
| 164 |
-
c => c.symbol.toUpperCase() === symbol.toUpperCase()
|
| 165 |
-
);
|
| 166 |
-
return coin ? { ok: true, data: coin } : { ok: false, error: 'Coin not found' };
|
| 167 |
-
}
|
| 168 |
-
return result;
|
| 169 |
-
});
|
| 170 |
}
|
| 171 |
|
| 172 |
getMarketStats() {
|
| 173 |
-
|
| 174 |
-
return this.get('/api/market').then(result => {
|
| 175 |
-
if (result.ok && result.data) {
|
| 176 |
-
return {
|
| 177 |
-
ok: true,
|
| 178 |
-
data: {
|
| 179 |
-
total_market_cap: result.data.total_market_cap,
|
| 180 |
-
btc_dominance: result.data.btc_dominance,
|
| 181 |
-
total_volume_24h: result.data.total_volume_24h,
|
| 182 |
-
market_cap_change_24h: result.data.market_cap_change_24h
|
| 183 |
-
}
|
| 184 |
-
};
|
| 185 |
-
}
|
| 186 |
-
return result;
|
| 187 |
-
});
|
| 188 |
}
|
| 189 |
|
| 190 |
getLatestNews(limit = 20) {
|
| 191 |
-
|
| 192 |
-
return Promise.resolve({
|
| 193 |
-
ok: true,
|
| 194 |
-
data: {
|
| 195 |
-
articles: [],
|
| 196 |
-
message: 'News endpoint not yet implemented in backend'
|
| 197 |
-
}
|
| 198 |
-
});
|
| 199 |
}
|
| 200 |
|
| 201 |
getProviders() {
|
|
@@ -203,110 +153,41 @@ class ApiClient {
|
|
| 203 |
}
|
| 204 |
|
| 205 |
getPriceChart(symbol, timeframe = '7d') {
|
| 206 |
-
|
| 207 |
-
const cleanSymbol = encodeURIComponent(String(symbol || 'BTC').trim().toUpperCase());
|
| 208 |
-
// Map timeframe to limit (rough approximation)
|
| 209 |
-
const limitMap = { '1d': 24, '7d': 168, '30d': 720, '90d': 2160, '365d': 8760 };
|
| 210 |
-
const limit = limitMap[timeframe] || 168;
|
| 211 |
-
return this.get(`/api/market/history?symbol=${cleanSymbol}&limit=${limit}`);
|
| 212 |
}
|
| 213 |
|
| 214 |
analyzeChart(symbol, timeframe = '7d', indicators = []) {
|
| 215 |
-
|
| 216 |
-
return Promise.resolve({
|
| 217 |
-
ok: false,
|
| 218 |
-
error: 'Chart analysis not yet implemented in backend'
|
| 219 |
-
});
|
| 220 |
}
|
| 221 |
|
| 222 |
runQuery(payload) {
|
| 223 |
-
|
| 224 |
-
return Promise.resolve({
|
| 225 |
-
ok: false,
|
| 226 |
-
error: 'Query endpoint not yet implemented in backend'
|
| 227 |
-
});
|
| 228 |
}
|
| 229 |
|
| 230 |
analyzeSentiment(payload) {
|
| 231 |
-
|
| 232 |
-
// For now, return the market sentiment
|
| 233 |
-
return this.get('/api/sentiment');
|
| 234 |
}
|
| 235 |
|
| 236 |
summarizeNews(item) {
|
| 237 |
-
|
| 238 |
-
return Promise.resolve({
|
| 239 |
-
ok: false,
|
| 240 |
-
error: 'News summarization not yet implemented in backend'
|
| 241 |
-
});
|
| 242 |
}
|
| 243 |
|
| 244 |
getDatasetsList() {
|
| 245 |
-
|
| 246 |
-
return Promise.resolve({
|
| 247 |
-
ok: true,
|
| 248 |
-
data: {
|
| 249 |
-
datasets: [],
|
| 250 |
-
message: 'Datasets endpoint not yet implemented in backend'
|
| 251 |
-
}
|
| 252 |
-
});
|
| 253 |
}
|
| 254 |
|
| 255 |
getDatasetSample(name) {
|
| 256 |
-
|
| 257 |
-
return Promise.resolve({
|
| 258 |
-
ok: false,
|
| 259 |
-
error: 'Dataset sample not yet implemented in backend'
|
| 260 |
-
});
|
| 261 |
}
|
| 262 |
|
| 263 |
getModelsList() {
|
| 264 |
-
|
| 265 |
-
return this.get('/api/hf/models');
|
| 266 |
}
|
| 267 |
|
| 268 |
testModel(payload) {
|
| 269 |
-
|
| 270 |
-
return Promise.resolve({
|
| 271 |
-
ok: false,
|
| 272 |
-
error: 'Model testing not yet implemented in backend'
|
| 273 |
-
});
|
| 274 |
-
}
|
| 275 |
-
|
| 276 |
-
// ===== Additional methods for backend compatibility =====
|
| 277 |
-
|
| 278 |
-
getTrending() {
|
| 279 |
-
return this.get('/api/trending');
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
getStats() {
|
| 283 |
-
return this.get('/api/stats');
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
getHFHealth() {
|
| 287 |
-
return this.get('/api/hf/health');
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
runDiagnostics(autoFix = false) {
|
| 291 |
-
return this.post('/api/diagnostics/run', { auto_fix: autoFix });
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
getLastDiagnostics() {
|
| 295 |
-
return this.get('/api/diagnostics/last');
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
runAPLScan() {
|
| 299 |
-
return this.post('/api/apl/run');
|
| 300 |
-
}
|
| 301 |
-
|
| 302 |
-
getAPLReport() {
|
| 303 |
-
return this.get('/api/apl/report');
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
getAPLSummary() {
|
| 307 |
-
return this.get('/api/apl/summary');
|
| 308 |
}
|
| 309 |
}
|
| 310 |
|
| 311 |
const apiClient = new ApiClient();
|
| 312 |
-
export default apiClient;
|
|
|
|
| 2 |
|
| 3 |
class ApiClient {
|
| 4 |
constructor() {
|
| 5 |
+
const origin = window?.location?.origin ?? '';
|
| 6 |
+
this.baseURL = origin.replace(/\/$/, '');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
this.cache = new Map();
|
| 8 |
this.requestLogs = [];
|
| 9 |
this.errorLogs = [];
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
// ===== Specific API helpers =====
|
|
|
|
|
|
|
| 131 |
getHealth() {
|
| 132 |
+
return this.get('/api/health');
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
getTopCoins(limit = 10) {
|
| 136 |
+
return this.get(`/api/coins/top?limit=${limit}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
|
| 139 |
getCoinDetails(symbol) {
|
| 140 |
+
return this.get(`/api/coins/${symbol}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
}
|
| 142 |
|
| 143 |
getMarketStats() {
|
| 144 |
+
return this.get('/api/market/stats');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
getLatestNews(limit = 20) {
|
| 148 |
+
return this.get(`/api/news/latest?limit=${limit}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
}
|
| 150 |
|
| 151 |
getProviders() {
|
|
|
|
| 153 |
}
|
| 154 |
|
| 155 |
getPriceChart(symbol, timeframe = '7d') {
|
| 156 |
+
return this.get(`/api/charts/price/${symbol}?timeframe=${timeframe}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
| 159 |
analyzeChart(symbol, timeframe = '7d', indicators = []) {
|
| 160 |
+
return this.post('/api/charts/analyze', { symbol, timeframe, indicators });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
|
| 163 |
runQuery(payload) {
|
| 164 |
+
return this.post('/api/query', payload);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
analyzeSentiment(payload) {
|
| 168 |
+
return this.post('/api/sentiment/analyze', payload);
|
|
|
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
summarizeNews(item) {
|
| 172 |
+
return this.post('/api/news/summarize', item);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
|
| 175 |
getDatasetsList() {
|
| 176 |
+
return this.get('/api/datasets/list');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
|
| 179 |
getDatasetSample(name) {
|
| 180 |
+
return this.get(`/api/datasets/sample?name=${encodeURIComponent(name)}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
}
|
| 182 |
|
| 183 |
getModelsList() {
|
| 184 |
+
return this.get('/api/models/list');
|
|
|
|
| 185 |
}
|
| 186 |
|
| 187 |
testModel(payload) {
|
| 188 |
+
return this.post('/api/models/test', payload);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
}
|
| 191 |
|
| 192 |
const apiClient = new ApiClient();
|
| 193 |
+
export default apiClient;
|
static/js/apiExplorerView.js
CHANGED
|
@@ -54,10 +54,8 @@ class ApiExplorerView {
|
|
| 54 |
if (this.bodyInput) {
|
| 55 |
this.bodyInput.value = preset.body || '';
|
| 56 |
}
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
if (descEl) descEl.textContent = preset.description;
|
| 60 |
-
if (pathEl) pathEl.textContent = preset.path;
|
| 61 |
}
|
| 62 |
|
| 63 |
async sendRequest() {
|
|
|
|
| 54 |
if (this.bodyInput) {
|
| 55 |
this.bodyInput.value = preset.body || '';
|
| 56 |
}
|
| 57 |
+
this.section.querySelector('[data-api-description]').textContent = preset.description;
|
| 58 |
+
this.section.querySelector('[data-api-path]').textContent = preset.path;
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
async sendRequest() {
|
static/js/app.js
CHANGED
|
@@ -1,955 +1,98 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
class WebSocketClient {
|
| 24 |
-
constructor(url) {
|
| 25 |
-
this.url = url;
|
| 26 |
-
this.socket = null;
|
| 27 |
-
this.status = 'disconnected';
|
| 28 |
-
this.reconnectAttempts = 0;
|
| 29 |
-
this.maxReconnectAttempts = 5;
|
| 30 |
-
this.reconnectDelay = 3000;
|
| 31 |
-
this.listeners = new Map();
|
| 32 |
-
this.heartbeatInterval = null;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
connect() {
|
| 36 |
-
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
| 37 |
-
console.log('[WS] Already connected');
|
| 38 |
-
return;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
try {
|
| 42 |
-
console.log('[WS] Connecting to:', this.url);
|
| 43 |
-
this.socket = new WebSocket(this.url);
|
| 44 |
-
|
| 45 |
-
this.socket.onopen = this.handleOpen.bind(this);
|
| 46 |
-
this.socket.onmessage = this.handleMessage.bind(this);
|
| 47 |
-
this.socket.onerror = this.handleError.bind(this);
|
| 48 |
-
this.socket.onclose = this.handleClose.bind(this);
|
| 49 |
-
|
| 50 |
-
this.updateStatus('connecting');
|
| 51 |
-
} catch (error) {
|
| 52 |
-
console.error('[WS] Connection error:', error);
|
| 53 |
-
this.scheduleReconnect();
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
handleOpen() {
|
| 58 |
-
console.log('[WS] Connected successfully');
|
| 59 |
-
this.status = 'connected';
|
| 60 |
-
this.reconnectAttempts = 0;
|
| 61 |
-
this.updateStatus('connected');
|
| 62 |
-
this.startHeartbeat();
|
| 63 |
-
this.emit('connected', true);
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
handleMessage(event) {
|
| 67 |
-
try {
|
| 68 |
-
const data = JSON.parse(event.data);
|
| 69 |
-
console.log('[WS] Message received:', data.type);
|
| 70 |
-
|
| 71 |
-
if (data.type === 'heartbeat') {
|
| 72 |
-
this.send({ type: 'pong' });
|
| 73 |
-
return;
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
this.emit(data.type, data);
|
| 77 |
-
this.emit('message', data);
|
| 78 |
-
} catch (error) {
|
| 79 |
-
console.error('[WS] Message parse error:', error);
|
| 80 |
-
}
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
handleError(error) {
|
| 84 |
-
console.error('[WS] Error:', error);
|
| 85 |
-
this.updateStatus('error');
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
handleClose() {
|
| 89 |
-
console.log('[WS] Connection closed');
|
| 90 |
-
this.status = 'disconnected';
|
| 91 |
-
this.updateStatus('disconnected');
|
| 92 |
-
this.stopHeartbeat();
|
| 93 |
-
this.emit('connected', false);
|
| 94 |
-
this.scheduleReconnect();
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
scheduleReconnect() {
|
| 98 |
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
| 99 |
-
console.error('[WS] Max reconnection attempts reached');
|
| 100 |
-
return;
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
this.reconnectAttempts++;
|
| 104 |
-
console.log(`[WS] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`);
|
| 105 |
-
|
| 106 |
-
setTimeout(() => this.connect(), this.reconnectDelay);
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
startHeartbeat() {
|
| 110 |
-
this.heartbeatInterval = setInterval(() => {
|
| 111 |
-
if (this.isConnected()) {
|
| 112 |
-
this.send({ type: 'ping' });
|
| 113 |
-
}
|
| 114 |
-
}, 30000);
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
stopHeartbeat() {
|
| 118 |
-
if (this.heartbeatInterval) {
|
| 119 |
-
clearInterval(this.heartbeatInterval);
|
| 120 |
-
this.heartbeatInterval = null;
|
| 121 |
-
}
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
send(data) {
|
| 125 |
-
if (this.isConnected()) {
|
| 126 |
-
this.socket.send(JSON.stringify(data));
|
| 127 |
-
return true;
|
| 128 |
-
}
|
| 129 |
-
console.warn('[WS] Cannot send - not connected');
|
| 130 |
-
return false;
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
on(event, callback) {
|
| 134 |
-
if (!this.listeners.has(event)) {
|
| 135 |
-
this.listeners.set(event, []);
|
| 136 |
-
}
|
| 137 |
-
this.listeners.get(event).push(callback);
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
emit(event, data) {
|
| 141 |
-
if (this.listeners.has(event)) {
|
| 142 |
-
this.listeners.get(event).forEach(callback => callback(data));
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
updateStatus(status) {
|
| 147 |
-
this.status = status;
|
| 148 |
-
|
| 149 |
-
const statusBar = document.getElementById('connection-status-bar');
|
| 150 |
-
const statusDot = document.getElementById('ws-status-dot');
|
| 151 |
-
const statusText = document.getElementById('ws-status-text');
|
| 152 |
-
|
| 153 |
-
if (statusBar && statusDot && statusText) {
|
| 154 |
-
if (status === 'connected') {
|
| 155 |
-
statusBar.classList.remove('disconnected');
|
| 156 |
-
statusText.textContent = 'متصل';
|
| 157 |
-
} else if (status === 'disconnected' || status === 'error') {
|
| 158 |
-
statusBar.classList.add('disconnected');
|
| 159 |
-
statusText.textContent = status === 'error' ? 'خطا در اتصال' : 'قطع شده';
|
| 160 |
-
} else {
|
| 161 |
-
statusText.textContent = 'در حال اتصال...';
|
| 162 |
-
}
|
| 163 |
-
}
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
isConnected() {
|
| 167 |
-
return this.socket && this.socket.readyState === WebSocket.OPEN;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
disconnect() {
|
| 171 |
-
if (this.socket) {
|
| 172 |
-
this.socket.close();
|
| 173 |
-
}
|
| 174 |
-
this.stopHeartbeat();
|
| 175 |
-
}
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 179 |
-
// API CLIENT
|
| 180 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 181 |
-
|
| 182 |
-
class APIClient {
|
| 183 |
-
constructor(baseURL) {
|
| 184 |
-
this.baseURL = baseURL;
|
| 185 |
-
this.cache = new Map();
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
async request(endpoint, options = {}) {
|
| 189 |
-
const url = `${this.baseURL}${endpoint}`;
|
| 190 |
-
const cacheKey = `${options.method || 'GET'}:${url}`;
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
}
|
| 199 |
-
}
|
| 200 |
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
...options.headers,
|
| 208 |
-
},
|
| 209 |
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
| 210 |
});
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
const data = await response.json();
|
| 217 |
-
|
| 218 |
-
// Cache successful GET requests
|
| 219 |
-
if (!options.method || options.method === 'GET') {
|
| 220 |
-
this.cache.set(cacheKey, {
|
| 221 |
-
data,
|
| 222 |
-
timestamp: Date.now(),
|
| 223 |
-
});
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
return data;
|
| 227 |
-
} catch (error) {
|
| 228 |
-
console.error('[API] Error:', endpoint, error);
|
| 229 |
-
throw error;
|
| 230 |
-
}
|
| 231 |
-
}
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
return this.request('/api/market', { cache: true });
|
| 236 |
-
}
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
}
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
}
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
}
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
return this.request(`/api/news?limit=${limit}`, { cache: true });
|
| 253 |
-
}
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
return this.request('/api/providers', { cache: true });
|
| 258 |
-
}
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
return this.request(`/api/market/history?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true });
|
| 263 |
-
}
|
| 264 |
-
}
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 269 |
|
| 270 |
-
const
|
| 271 |
-
|
| 272 |
-
if (value === null || value === undefined || isNaN(value)) {
|
| 273 |
-
return '—';
|
| 274 |
-
}
|
| 275 |
-
const num = Number(value);
|
| 276 |
-
if (Math.abs(num) >= 1e12) {
|
| 277 |
-
return `$${(num / 1e12).toFixed(2)}T`;
|
| 278 |
-
}
|
| 279 |
-
if (Math.abs(num) >= 1e9) {
|
| 280 |
-
return `$${(num / 1e9).toFixed(2)}B`;
|
| 281 |
-
}
|
| 282 |
-
if (Math.abs(num) >= 1e6) {
|
| 283 |
-
return `$${(num / 1e6).toFixed(2)}M`;
|
| 284 |
-
}
|
| 285 |
-
if (Math.abs(num) >= 1e3) {
|
| 286 |
-
return `$${(num / 1e3).toFixed(2)}K`;
|
| 287 |
-
}
|
| 288 |
-
return `$${num.toLocaleString(undefined, {
|
| 289 |
-
minimumFractionDigits: 2,
|
| 290 |
-
maximumFractionDigits: 2
|
| 291 |
-
})}`;
|
| 292 |
},
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
formatNumber(value) {
|
| 304 |
-
if (value === null || value === undefined || isNaN(value)) {
|
| 305 |
-
return '—';
|
| 306 |
-
}
|
| 307 |
-
return Number(value).toLocaleString();
|
| 308 |
-
},
|
| 309 |
-
|
| 310 |
-
formatDate(timestamp) {
|
| 311 |
-
const date = new Date(timestamp);
|
| 312 |
-
return date.toLocaleDateString('fa-IR', {
|
| 313 |
-
year: 'numeric',
|
| 314 |
-
month: 'long',
|
| 315 |
-
day: 'numeric',
|
| 316 |
-
hour: '2-digit',
|
| 317 |
-
minute: '2-digit',
|
| 318 |
});
|
| 319 |
},
|
| 320 |
|
| 321 |
-
|
| 322 |
-
if (
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
در حال بارگذاری...
|
| 333 |
-
</div>
|
| 334 |
-
`;
|
| 335 |
-
}
|
| 336 |
-
},
|
| 337 |
-
|
| 338 |
-
showError(element, message) {
|
| 339 |
-
if (element) {
|
| 340 |
-
element.innerHTML = `
|
| 341 |
-
<div class="error-message">
|
| 342 |
-
<i class="fas fa-exclamation-circle"></i>
|
| 343 |
-
${message}
|
| 344 |
-
</div>
|
| 345 |
-
`;
|
| 346 |
}
|
| 347 |
},
|
| 348 |
-
|
| 349 |
-
debounce(func, wait) {
|
| 350 |
-
let timeout;
|
| 351 |
-
return function executedFunction(...args) {
|
| 352 |
-
const later = () => {
|
| 353 |
-
clearTimeout(timeout);
|
| 354 |
-
func(...args);
|
| 355 |
-
};
|
| 356 |
-
clearTimeout(timeout);
|
| 357 |
-
timeout = setTimeout(later, wait);
|
| 358 |
-
};
|
| 359 |
-
},
|
| 360 |
};
|
| 361 |
|
| 362 |
-
|
| 363 |
-
// VIEW MANAGER
|
| 364 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 365 |
-
|
| 366 |
-
class ViewManager {
|
| 367 |
-
constructor() {
|
| 368 |
-
this.currentView = 'overview';
|
| 369 |
-
this.views = new Map();
|
| 370 |
-
this.init();
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
init() {
|
| 374 |
-
// Desktop navigation
|
| 375 |
-
document.querySelectorAll('.nav-tab-btn').forEach(btn => {
|
| 376 |
-
btn.addEventListener('click', (e) => {
|
| 377 |
-
const view = btn.dataset.view;
|
| 378 |
-
this.switchView(view);
|
| 379 |
-
});
|
| 380 |
-
});
|
| 381 |
-
|
| 382 |
-
// Mobile navigation
|
| 383 |
-
document.querySelectorAll('.mobile-nav-tab-btn').forEach(btn => {
|
| 384 |
-
btn.addEventListener('click', (e) => {
|
| 385 |
-
const view = btn.dataset.view;
|
| 386 |
-
this.switchView(view);
|
| 387 |
-
});
|
| 388 |
-
});
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
switchView(viewName) {
|
| 392 |
-
if (this.currentView === viewName) return;
|
| 393 |
-
|
| 394 |
-
// Hide all views
|
| 395 |
-
document.querySelectorAll('.view-section').forEach(section => {
|
| 396 |
-
section.classList.remove('active');
|
| 397 |
-
});
|
| 398 |
-
|
| 399 |
-
// Show selected view
|
| 400 |
-
const viewSection = document.getElementById(`view-${viewName}`);
|
| 401 |
-
if (viewSection) {
|
| 402 |
-
viewSection.classList.add('active');
|
| 403 |
-
}
|
| 404 |
-
|
| 405 |
-
// Update navigation buttons
|
| 406 |
-
document.querySelectorAll('.nav-tab-btn, .mobile-nav-tab-btn').forEach(btn => {
|
| 407 |
-
btn.classList.remove('active');
|
| 408 |
-
if (btn.dataset.view === viewName) {
|
| 409 |
-
btn.classList.add('active');
|
| 410 |
-
}
|
| 411 |
-
});
|
| 412 |
-
|
| 413 |
-
this.currentView = viewName;
|
| 414 |
-
console.log('[View] Switched to:', viewName);
|
| 415 |
-
|
| 416 |
-
// Trigger view-specific updates
|
| 417 |
-
this.triggerViewUpdate(viewName);
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
triggerViewUpdate(viewName) {
|
| 421 |
-
const event = new CustomEvent('viewChange', { detail: { view: viewName } });
|
| 422 |
-
document.dispatchEvent(event);
|
| 423 |
-
}
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 427 |
-
// DASHBOARD APPLICATION
|
| 428 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 429 |
-
|
| 430 |
-
class DashboardApp {
|
| 431 |
-
constructor() {
|
| 432 |
-
this.ws = new WebSocketClient(CONFIG.WS_URL);
|
| 433 |
-
this.api = new APIClient(CONFIG.BACKEND_URL);
|
| 434 |
-
this.viewManager = new ViewManager();
|
| 435 |
-
this.updateInterval = null;
|
| 436 |
-
this.data = {
|
| 437 |
-
market: null,
|
| 438 |
-
sentiment: null,
|
| 439 |
-
trending: null,
|
| 440 |
-
news: [],
|
| 441 |
-
};
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
async init() {
|
| 445 |
-
console.log('[App] Initializing dashboard...');
|
| 446 |
-
|
| 447 |
-
// Connect WebSocket
|
| 448 |
-
this.ws.connect();
|
| 449 |
-
this.setupWebSocketHandlers();
|
| 450 |
-
|
| 451 |
-
// Setup UI handlers
|
| 452 |
-
this.setupUIHandlers();
|
| 453 |
-
|
| 454 |
-
// Load initial data
|
| 455 |
-
await this.loadInitialData();
|
| 456 |
-
|
| 457 |
-
// Start periodic updates
|
| 458 |
-
this.startPeriodicUpdates();
|
| 459 |
-
|
| 460 |
-
console.log('[App] Dashboard initialized successfully');
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
setupWebSocketHandlers() {
|
| 464 |
-
this.ws.on('connected', (isConnected) => {
|
| 465 |
-
console.log('[App] WebSocket connection status:', isConnected);
|
| 466 |
-
if (isConnected) {
|
| 467 |
-
this.ws.send({ type: 'subscribe', groups: ['market', 'sentiment'] });
|
| 468 |
-
}
|
| 469 |
-
});
|
| 470 |
-
|
| 471 |
-
this.ws.on('market_update', (data) => {
|
| 472 |
-
console.log('[App] Market update received');
|
| 473 |
-
this.handleMarketUpdate(data);
|
| 474 |
-
});
|
| 475 |
-
|
| 476 |
-
this.ws.on('sentiment_update', (data) => {
|
| 477 |
-
console.log('[App] Sentiment update received');
|
| 478 |
-
this.handleSentimentUpdate(data);
|
| 479 |
-
});
|
| 480 |
-
|
| 481 |
-
this.ws.on('stats_update', (data) => {
|
| 482 |
-
console.log('[App] Stats update received');
|
| 483 |
-
this.updateOnlineUsers(data.active_connections || 0);
|
| 484 |
-
});
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
setupUIHandlers() {
|
| 488 |
-
// Theme toggle
|
| 489 |
-
const themeToggle = document.getElementById('theme-toggle');
|
| 490 |
-
if (themeToggle) {
|
| 491 |
-
themeToggle.addEventListener('click', () => this.toggleTheme());
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
// Notifications
|
| 495 |
-
const notificationsBtn = document.getElementById('notifications-btn');
|
| 496 |
-
const notificationsPanel = document.getElementById('notifications-panel');
|
| 497 |
-
const closeNotifications = document.getElementById('close-notifications');
|
| 498 |
-
|
| 499 |
-
if (notificationsBtn && notificationsPanel) {
|
| 500 |
-
notificationsBtn.addEventListener('click', () => {
|
| 501 |
-
notificationsPanel.classList.toggle('active');
|
| 502 |
-
});
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
if (closeNotifications && notificationsPanel) {
|
| 506 |
-
closeNotifications.addEventListener('click', () => {
|
| 507 |
-
notificationsPanel.classList.remove('active');
|
| 508 |
-
});
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
// Refresh buttons
|
| 512 |
-
const refreshCoins = document.getElementById('refresh-coins');
|
| 513 |
-
if (refreshCoins) {
|
| 514 |
-
refreshCoins.addEventListener('click', () => this.loadMarketData());
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
// Floating stats minimize
|
| 518 |
-
const minimizeStats = document.getElementById('minimize-stats');
|
| 519 |
-
const floatingStats = document.getElementById('floating-stats');
|
| 520 |
-
if (minimizeStats && floatingStats) {
|
| 521 |
-
minimizeStats.addEventListener('click', () => {
|
| 522 |
-
floatingStats.classList.toggle('minimized');
|
| 523 |
-
});
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
// Global search
|
| 527 |
-
const globalSearch = document.getElementById('global-search');
|
| 528 |
-
if (globalSearch) {
|
| 529 |
-
globalSearch.addEventListener('input', Utils.debounce((e) => {
|
| 530 |
-
this.handleSearch(e.target.value);
|
| 531 |
-
}, 300));
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
// AI Tools
|
| 535 |
-
this.setupAIToolHandlers();
|
| 536 |
-
}
|
| 537 |
-
|
| 538 |
-
setupAIToolHandlers() {
|
| 539 |
-
const sentimentBtn = document.getElementById('sentiment-analysis-btn');
|
| 540 |
-
const summaryBtn = document.getElementById('news-summary-btn');
|
| 541 |
-
const predictionBtn = document.getElementById('price-prediction-btn');
|
| 542 |
-
const patternBtn = document.getElementById('pattern-detection-btn');
|
| 543 |
-
|
| 544 |
-
if (sentimentBtn) {
|
| 545 |
-
sentimentBtn.addEventListener('click', () => this.runSentimentAnalysis());
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
if (summaryBtn) {
|
| 549 |
-
summaryBtn.addEventListener('click', () => this.runNewsSummary());
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
if (predictionBtn) {
|
| 553 |
-
predictionBtn.addEventListener('click', () => this.runPricePrediction());
|
| 554 |
-
}
|
| 555 |
-
|
| 556 |
-
if (patternBtn) {
|
| 557 |
-
patternBtn.addEventListener('click', () => this.runPatternDetection());
|
| 558 |
-
}
|
| 559 |
-
|
| 560 |
-
const clearResults = document.getElementById('clear-results');
|
| 561 |
-
const aiResults = document.getElementById('ai-results');
|
| 562 |
-
if (clearResults && aiResults) {
|
| 563 |
-
clearResults.addEventListener('click', () => {
|
| 564 |
-
aiResults.style.display = 'none';
|
| 565 |
-
});
|
| 566 |
-
}
|
| 567 |
-
}
|
| 568 |
-
|
| 569 |
-
async loadInitialData() {
|
| 570 |
-
this.showLoadingOverlay(true);
|
| 571 |
-
|
| 572 |
-
try {
|
| 573 |
-
await Promise.all([
|
| 574 |
-
this.loadMarketData(),
|
| 575 |
-
this.loadSentimentData(),
|
| 576 |
-
this.loadTrendingData(),
|
| 577 |
-
this.loadNewsData(),
|
| 578 |
-
]);
|
| 579 |
-
} catch (error) {
|
| 580 |
-
console.error('[App] Error loading initial data:', error);
|
| 581 |
-
}
|
| 582 |
-
|
| 583 |
-
this.showLoadingOverlay(false);
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
async loadMarketData() {
|
| 587 |
-
try {
|
| 588 |
-
const data = await this.api.getMarket();
|
| 589 |
-
this.data.market = data;
|
| 590 |
-
this.renderMarketStats(data);
|
| 591 |
-
this.renderCoinsTable(data.cryptocurrencies || []);
|
| 592 |
-
} catch (error) {
|
| 593 |
-
console.error('[App] Error loading market data:', error);
|
| 594 |
-
}
|
| 595 |
-
}
|
| 596 |
-
|
| 597 |
-
async loadSentimentData() {
|
| 598 |
-
try {
|
| 599 |
-
const data = await this.api.getSentiment();
|
| 600 |
-
this.data.sentiment = data;
|
| 601 |
-
this.renderSentiment(data);
|
| 602 |
-
} catch (error) {
|
| 603 |
-
console.error('[App] Error loading sentiment data:', error);
|
| 604 |
-
}
|
| 605 |
-
}
|
| 606 |
-
|
| 607 |
-
async loadTrendingData() {
|
| 608 |
-
try {
|
| 609 |
-
const data = await this.api.getTrending();
|
| 610 |
-
this.data.trending = data;
|
| 611 |
-
} catch (error) {
|
| 612 |
-
console.error('[App] Error loading trending data:', error);
|
| 613 |
-
}
|
| 614 |
-
}
|
| 615 |
-
|
| 616 |
-
async loadNewsData() {
|
| 617 |
-
try {
|
| 618 |
-
const data = await this.api.getNews(20);
|
| 619 |
-
this.data.news = data.news || [];
|
| 620 |
-
this.renderNews(this.data.news);
|
| 621 |
-
} catch (error) {
|
| 622 |
-
console.error('[App] Error loading news data:', error);
|
| 623 |
-
}
|
| 624 |
-
}
|
| 625 |
-
|
| 626 |
-
renderMarketStats(data) {
|
| 627 |
-
const totalMarketCap = document.getElementById('total-market-cap');
|
| 628 |
-
const btcDominance = document.getElementById('btc-dominance');
|
| 629 |
-
const volume24h = document.getElementById('volume-24h');
|
| 630 |
-
|
| 631 |
-
if (totalMarketCap && data.total_market_cap) {
|
| 632 |
-
totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap);
|
| 633 |
-
}
|
| 634 |
-
|
| 635 |
-
if (btcDominance && data.btc_dominance) {
|
| 636 |
-
btcDominance.textContent = `${data.btc_dominance.toFixed(1)}%`;
|
| 637 |
-
}
|
| 638 |
-
|
| 639 |
-
if (volume24h && data.total_volume_24h) {
|
| 640 |
-
volume24h.textContent = Utils.formatCurrency(data.total_volume_24h);
|
| 641 |
-
}
|
| 642 |
-
}
|
| 643 |
-
|
| 644 |
-
renderCoinsTable(coins) {
|
| 645 |
-
const tbody = document.getElementById('coins-table-body');
|
| 646 |
-
if (!tbody) return;
|
| 647 |
-
|
| 648 |
-
if (!coins || coins.length === 0) {
|
| 649 |
-
tbody.innerHTML = '<tr><td colspan="7">دادهای یافت نشد</td></tr>';
|
| 650 |
-
return;
|
| 651 |
-
}
|
| 652 |
-
|
| 653 |
-
tbody.innerHTML = coins.slice(0, 20).map((coin, index) => `
|
| 654 |
-
<tr>
|
| 655 |
-
<td>${index + 1}</td>
|
| 656 |
-
<td>
|
| 657 |
-
<div style="display: flex; align-items: center; gap: 8px;">
|
| 658 |
-
<strong>${coin.symbol}</strong>
|
| 659 |
-
<span style="color: var(--text-muted); font-size: 0.875rem;">${coin.name}</span>
|
| 660 |
-
</div>
|
| 661 |
-
</td>
|
| 662 |
-
<td style="font-family: var(--font-mono);">${Utils.formatCurrency(coin.current_price)}</td>
|
| 663 |
-
<td>
|
| 664 |
-
<span class="stat-change ${Utils.getChangeClass(coin.price_change_percentage_24h)}">
|
| 665 |
-
${Utils.formatPercent(coin.price_change_percentage_24h)}
|
| 666 |
-
</span>
|
| 667 |
-
</td>
|
| 668 |
-
<td>${Utils.formatCurrency(coin.total_volume)}</td>
|
| 669 |
-
<td>${Utils.formatCurrency(coin.market_cap)}</td>
|
| 670 |
-
<td>
|
| 671 |
-
<button class="btn-ghost" onclick="app.viewCoinDetails('${coin.symbol}')">
|
| 672 |
-
<i class="fas fa-chart-line"></i>
|
| 673 |
-
</button>
|
| 674 |
-
</td>
|
| 675 |
-
</tr>
|
| 676 |
-
`).join('');
|
| 677 |
-
}
|
| 678 |
-
|
| 679 |
-
renderSentiment(data) {
|
| 680 |
-
if (!data) return;
|
| 681 |
-
|
| 682 |
-
const bullish = data.bullish || 0;
|
| 683 |
-
const neutral = data.neutral || 0;
|
| 684 |
-
const bearish = data.bearish || 0;
|
| 685 |
-
|
| 686 |
-
const bullishPercent = document.getElementById('bullish-percent');
|
| 687 |
-
const neutralPercent = document.getElementById('neutral-percent');
|
| 688 |
-
const bearishPercent = document.getElementById('bearish-percent');
|
| 689 |
-
|
| 690 |
-
if (bullishPercent) bullishPercent.textContent = `${bullish}%`;
|
| 691 |
-
if (neutralPercent) neutralPercent.textContent = `${neutral}%`;
|
| 692 |
-
if (bearishPercent) bearishPercent.textContent = `${bearish}%`;
|
| 693 |
-
|
| 694 |
-
// Update progress bars
|
| 695 |
-
const progressBars = document.querySelectorAll('.sentiment-progress-bar');
|
| 696 |
-
progressBars.forEach(bar => {
|
| 697 |
-
if (bar.classList.contains('bullish')) {
|
| 698 |
-
bar.style.width = `${bullish}%`;
|
| 699 |
-
} else if (bar.classList.contains('neutral')) {
|
| 700 |
-
bar.style.width = `${neutral}%`;
|
| 701 |
-
} else if (bar.classList.contains('bearish')) {
|
| 702 |
-
bar.style.width = `${bearish}%`;
|
| 703 |
-
}
|
| 704 |
-
});
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
renderNews(news) {
|
| 708 |
-
const newsGrid = document.getElementById('news-grid');
|
| 709 |
-
if (!newsGrid) return;
|
| 710 |
-
|
| 711 |
-
if (!news || news.length === 0) {
|
| 712 |
-
newsGrid.innerHTML = '<p>خبری یافت نشد</p>';
|
| 713 |
-
return;
|
| 714 |
-
}
|
| 715 |
-
|
| 716 |
-
newsGrid.innerHTML = news.map(item => `
|
| 717 |
-
<div class="news-card">
|
| 718 |
-
${item.image ? `<img src="${item.image}" alt="${item.title}" class="news-card-image">` : ''}
|
| 719 |
-
<div class="news-card-content">
|
| 720 |
-
<h3 class="news-card-title">${item.title}</h3>
|
| 721 |
-
<div class="news-card-meta">
|
| 722 |
-
<span><i class="fas fa-clock"></i> ${Utils.formatDate(item.published_at || Date.now())}</span>
|
| 723 |
-
<span><i class="fas fa-newspaper"></i> ${item.source || 'Unknown'}</span>
|
| 724 |
-
</div>
|
| 725 |
-
<p class="news-card-excerpt">${item.description || item.summary || ''}</p>
|
| 726 |
-
</div>
|
| 727 |
-
</div>
|
| 728 |
-
`).join('');
|
| 729 |
-
}
|
| 730 |
-
|
| 731 |
-
handleMarketUpdate(data) {
|
| 732 |
-
if (data.data) {
|
| 733 |
-
this.renderMarketStats(data.data);
|
| 734 |
-
if (data.data.cryptocurrencies) {
|
| 735 |
-
this.renderCoinsTable(data.data.cryptocurrencies);
|
| 736 |
-
}
|
| 737 |
-
}
|
| 738 |
-
}
|
| 739 |
-
|
| 740 |
-
handleSentimentUpdate(data) {
|
| 741 |
-
if (data.data) {
|
| 742 |
-
this.renderSentiment(data.data);
|
| 743 |
-
}
|
| 744 |
-
}
|
| 745 |
-
|
| 746 |
-
updateOnlineUsers(count) {
|
| 747 |
-
const activeUsersCount = document.getElementById('active-users-count');
|
| 748 |
-
if (activeUsersCount) {
|
| 749 |
-
activeUsersCount.textContent = count;
|
| 750 |
-
}
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
startPeriodicUpdates() {
|
| 754 |
-
this.updateInterval = setInterval(() => {
|
| 755 |
-
console.log('[App] Periodic update triggered');
|
| 756 |
-
this.loadMarketData();
|
| 757 |
-
this.loadSentimentData();
|
| 758 |
-
}, CONFIG.UPDATE_INTERVAL);
|
| 759 |
-
}
|
| 760 |
-
|
| 761 |
-
stopPeriodicUpdates() {
|
| 762 |
-
if (this.updateInterval) {
|
| 763 |
-
clearInterval(this.updateInterval);
|
| 764 |
-
this.updateInterval = null;
|
| 765 |
-
}
|
| 766 |
-
}
|
| 767 |
-
|
| 768 |
-
toggleTheme() {
|
| 769 |
-
document.body.classList.toggle('light-theme');
|
| 770 |
-
const icon = document.querySelector('#theme-toggle i');
|
| 771 |
-
if (icon) {
|
| 772 |
-
icon.classList.toggle('fa-moon');
|
| 773 |
-
icon.classList.toggle('fa-sun');
|
| 774 |
-
}
|
| 775 |
-
}
|
| 776 |
-
|
| 777 |
-
handleSearch(query) {
|
| 778 |
-
console.log('[App] Search query:', query);
|
| 779 |
-
// Implement search functionality
|
| 780 |
-
}
|
| 781 |
-
|
| 782 |
-
viewCoinDetails(symbol) {
|
| 783 |
-
console.log('[App] View coin details:', symbol);
|
| 784 |
-
// Switch to charts view and load coin data
|
| 785 |
-
this.viewManager.switchView('charts');
|
| 786 |
-
}
|
| 787 |
-
|
| 788 |
-
showLoadingOverlay(show) {
|
| 789 |
-
const overlay = document.getElementById('loading-overlay');
|
| 790 |
-
if (overlay) {
|
| 791 |
-
if (show) {
|
| 792 |
-
overlay.classList.add('active');
|
| 793 |
-
} else {
|
| 794 |
-
overlay.classList.remove('active');
|
| 795 |
-
}
|
| 796 |
-
}
|
| 797 |
-
}
|
| 798 |
-
|
| 799 |
-
// AI Tool Methods
|
| 800 |
-
async runSentimentAnalysis() {
|
| 801 |
-
const aiResults = document.getElementById('ai-results');
|
| 802 |
-
const aiResultsContent = document.getElementById('ai-results-content');
|
| 803 |
-
|
| 804 |
-
if (!aiResults || !aiResultsContent) return;
|
| 805 |
-
|
| 806 |
-
aiResults.style.display = 'block';
|
| 807 |
-
aiResultsContent.innerHTML = '<div class="loader"></div> در حال تحلیل...';
|
| 808 |
-
|
| 809 |
-
try {
|
| 810 |
-
const data = await this.api.getSentiment();
|
| 811 |
-
|
| 812 |
-
aiResultsContent.innerHTML = `
|
| 813 |
-
<div class="ai-result-card">
|
| 814 |
-
<h4>نتایج تحلیل احساسات</h4>
|
| 815 |
-
<div class="sentiment-summary">
|
| 816 |
-
<div class="sentiment-summary-item">
|
| 817 |
-
<div class="summary-label">صعودی</div>
|
| 818 |
-
<div class="summary-value bullish">${data.bullish}%</div>
|
| 819 |
-
</div>
|
| 820 |
-
<div class="sentiment-summary-item">
|
| 821 |
-
<div class="summary-label">خنثی</div>
|
| 822 |
-
<div class="summary-value neutral">${data.neutral}%</div>
|
| 823 |
-
</div>
|
| 824 |
-
<div class="sentiment-summary-item">
|
| 825 |
-
<div class="summary-label">نزولی</div>
|
| 826 |
-
<div class="summary-value bearish">${data.bearish}%</div>
|
| 827 |
-
</div>
|
| 828 |
-
</div>
|
| 829 |
-
<p style="margin-top: 1rem; color: var(--text-muted);">
|
| 830 |
-
${data.summary || 'تحلیل احساسات بازار بر اساس دادههای جمعآوری شده از منابع مختلف'}
|
| 831 |
-
</p>
|
| 832 |
-
</div>
|
| 833 |
-
`;
|
| 834 |
-
} catch (error) {
|
| 835 |
-
aiResultsContent.innerHTML = `
|
| 836 |
-
<div class="error-message">
|
| 837 |
-
<i class="fas fa-exclamation-circle"></i>
|
| 838 |
-
خطا در تحلیل: ${error.message}
|
| 839 |
-
</div>
|
| 840 |
-
`;
|
| 841 |
-
}
|
| 842 |
-
}
|
| 843 |
-
|
| 844 |
-
async runNewsSummary() {
|
| 845 |
-
const aiResults = document.getElementById('ai-results');
|
| 846 |
-
const aiResultsContent = document.getElementById('ai-results-content');
|
| 847 |
-
|
| 848 |
-
if (!aiResults || !aiResultsContent) return;
|
| 849 |
-
|
| 850 |
-
aiResults.style.display = 'block';
|
| 851 |
-
aiResultsContent.innerHTML = '<div class="loader"></div> در حال خلاصهسازی...';
|
| 852 |
-
|
| 853 |
-
setTimeout(() => {
|
| 854 |
-
aiResultsContent.innerHTML = `
|
| 855 |
-
<div class="ai-result-card">
|
| 856 |
-
<h4>خلاصه اخبار</h4>
|
| 857 |
-
<p>قابلیت خلاصهسازی اخبار به زودی اضافه خواهد شد.</p>
|
| 858 |
-
<p style="color: var(--text-muted); font-size: 0.875rem;">
|
| 859 |
-
این قابلیت از مدلهای Hugging Face برای خلاصهسازی متن استفاده میکند.
|
| 860 |
-
</p>
|
| 861 |
-
</div>
|
| 862 |
-
`;
|
| 863 |
-
}, 1000);
|
| 864 |
-
}
|
| 865 |
-
|
| 866 |
-
async runPricePrediction() {
|
| 867 |
-
const aiResults = document.getElementById('ai-results');
|
| 868 |
-
const aiResultsContent = document.getElementById('ai-results-content');
|
| 869 |
-
|
| 870 |
-
if (!aiResults || !aiResultsContent) return;
|
| 871 |
-
|
| 872 |
-
aiResults.style.display = 'block';
|
| 873 |
-
aiResultsContent.innerHTML = '<div class="loader"></div> در حال پیشبینی...';
|
| 874 |
-
|
| 875 |
-
setTimeout(() => {
|
| 876 |
-
aiResultsContent.innerHTML = `
|
| 877 |
-
<div class="ai-result-card">
|
| 878 |
-
<h4>پیشبینی قیمت</h4>
|
| 879 |
-
<p>قابلیت پیشبینی قیمت به زودی اضافه خواهد شد.</p>
|
| 880 |
-
<p style="color: var(--text-muted); font-size: 0.875rem;">
|
| 881 |
-
این قابلیت از مدلهای یادگیری ماشین برای پیشبینی روند قیمت استفاده میکند.
|
| 882 |
-
</p>
|
| 883 |
-
</div>
|
| 884 |
-
`;
|
| 885 |
-
}, 1000);
|
| 886 |
-
}
|
| 887 |
-
|
| 888 |
-
async runPatternDetection() {
|
| 889 |
-
const aiResults = document.getElementById('ai-results');
|
| 890 |
-
const aiResultsContent = document.getElementById('ai-results-content');
|
| 891 |
-
|
| 892 |
-
if (!aiResults || !aiResultsContent) return;
|
| 893 |
-
|
| 894 |
-
aiResults.style.display = 'block';
|
| 895 |
-
aiResultsContent.innerHTML = '<div class="loader"></div> در حال تشخیص الگو...';
|
| 896 |
-
|
| 897 |
-
setTimeout(() => {
|
| 898 |
-
aiResultsContent.innerHTML = `
|
| 899 |
-
<div class="ai-result-card">
|
| 900 |
-
<h4>تشخیص الگو</h4>
|
| 901 |
-
<p>قابلیت تشخیص الگو به زودی اضافه خواهد شد.</p>
|
| 902 |
-
<p style="color: var(--text-muted); font-size: 0.875rem;">
|
| 903 |
-
این قابلیت الگوهای کندل استیک و تحلیل تکنیکال را شناسایی میکند.
|
| 904 |
-
</p>
|
| 905 |
-
</div>
|
| 906 |
-
`;
|
| 907 |
-
}, 1000);
|
| 908 |
-
}
|
| 909 |
-
|
| 910 |
-
destroy() {
|
| 911 |
-
this.stopPeriodicUpdates();
|
| 912 |
-
this.ws.disconnect();
|
| 913 |
-
console.log('[App] Dashboard destroyed');
|
| 914 |
-
}
|
| 915 |
-
}
|
| 916 |
-
|
| 917 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 918 |
-
// INITIALIZATION
|
| 919 |
-
// ═══════════════════════════════════════════════════════════════════
|
| 920 |
-
|
| 921 |
-
let app;
|
| 922 |
-
|
| 923 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 924 |
-
console.log('[Main] DOM loaded, initializing application...');
|
| 925 |
-
|
| 926 |
-
app = new DashboardApp();
|
| 927 |
-
app.init();
|
| 928 |
-
|
| 929 |
-
// Make app globally accessible for debugging
|
| 930 |
-
window.app = app;
|
| 931 |
-
|
| 932 |
-
console.log('[Main] Application ready');
|
| 933 |
-
});
|
| 934 |
-
|
| 935 |
-
// Cleanup on page unload
|
| 936 |
-
window.addEventListener('beforeunload', () => {
|
| 937 |
-
if (app) {
|
| 938 |
-
app.destroy();
|
| 939 |
-
}
|
| 940 |
-
});
|
| 941 |
-
|
| 942 |
-
// Handle visibility change to pause/resume updates
|
| 943 |
-
document.addEventListener('visibilitychange', () => {
|
| 944 |
-
if (document.hidden) {
|
| 945 |
-
console.log('[Main] Page hidden, pausing updates');
|
| 946 |
-
app.stopPeriodicUpdates();
|
| 947 |
-
} else {
|
| 948 |
-
console.log('[Main] Page visible, resuming updates');
|
| 949 |
-
app.startPeriodicUpdates();
|
| 950 |
-
app.loadMarketData();
|
| 951 |
-
}
|
| 952 |
-
});
|
| 953 |
-
|
| 954 |
-
// Export for module usage
|
| 955 |
-
export { DashboardApp, APIClient, WebSocketClient, Utils };
|
|
|
|
| 1 |
+
import apiClient from './apiClient.js';
|
| 2 |
+
import wsClient from './wsClient.js';
|
| 3 |
+
import OverviewView from './overviewView.js';
|
| 4 |
+
import MarketView from './marketView.js';
|
| 5 |
+
import NewsView from './newsView.js';
|
| 6 |
+
import ChartLabView from './chartLabView.js';
|
| 7 |
+
import AIAdvisorView from './aiAdvisorView.js';
|
| 8 |
+
import DatasetsModelsView from './datasetsModelsView.js';
|
| 9 |
+
import DebugConsoleView from './debugConsoleView.js';
|
| 10 |
+
import SettingsView from './settingsView.js';
|
| 11 |
+
import ProvidersView from './providersView.js';
|
| 12 |
+
import ApiExplorerView from './apiExplorerView.js';
|
| 13 |
+
|
| 14 |
+
const App = {
|
| 15 |
+
init() {
|
| 16 |
+
this.cacheElements();
|
| 17 |
+
this.bindNavigation();
|
| 18 |
+
this.initViews();
|
| 19 |
+
this.initStatusBadges();
|
| 20 |
+
wsClient.connect();
|
| 21 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
+
cacheElements() {
|
| 24 |
+
this.sections = document.querySelectorAll('.page');
|
| 25 |
+
this.navButtons = document.querySelectorAll('[data-nav]');
|
| 26 |
+
this.apiHealthBadge = document.querySelector('[data-api-health]');
|
| 27 |
+
this.wsBadge = document.querySelector('[data-ws-status]');
|
| 28 |
+
},
|
|
|
|
|
|
|
| 29 |
|
| 30 |
+
bindNavigation() {
|
| 31 |
+
this.navButtons.forEach((button) => {
|
| 32 |
+
button.addEventListener('click', () => {
|
| 33 |
+
const target = button.dataset.nav;
|
| 34 |
+
this.sections.forEach((section) => section.classList.toggle('active', section.id === target));
|
| 35 |
+
this.navButtons.forEach((btn) => btn.classList.toggle('active', btn === button));
|
|
|
|
|
|
|
|
|
|
| 36 |
});
|
| 37 |
+
});
|
| 38 |
+
},
|
| 39 |
|
| 40 |
+
initViews() {
|
| 41 |
+
const overview = new OverviewView(document.getElementById('page-overview'));
|
| 42 |
+
overview.init();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
const market = new MarketView(document.getElementById('page-market'), wsClient);
|
| 45 |
+
market.init();
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
const news = new NewsView(document.getElementById('page-news'));
|
| 48 |
+
news.init();
|
|
|
|
| 49 |
|
| 50 |
+
const chartLab = new ChartLabView(document.getElementById('page-chart'));
|
| 51 |
+
chartLab.init();
|
|
|
|
| 52 |
|
| 53 |
+
const aiAdvisor = new AIAdvisorView(document.getElementById('page-ai'));
|
| 54 |
+
aiAdvisor.init();
|
|
|
|
| 55 |
|
| 56 |
+
const datasets = new DatasetsModelsView(document.getElementById('page-datasets'));
|
| 57 |
+
datasets.init();
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
const debugView = new DebugConsoleView(document.getElementById('page-debug'), wsClient);
|
| 60 |
+
debugView.init();
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
const settings = new SettingsView(document.getElementById('page-settings'));
|
| 63 |
+
settings.init();
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
const providersView = new ProvidersView(document.getElementById('page-providers'));
|
| 66 |
+
providersView.init();
|
|
|
|
| 67 |
|
| 68 |
+
const apiExplorer = new ApiExplorerView(document.getElementById('page-api'));
|
| 69 |
+
apiExplorer.init();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
},
|
| 71 |
|
| 72 |
+
initStatusBadges() {
|
| 73 |
+
this.refreshHealth();
|
| 74 |
+
wsClient.onStatusChange((status) => {
|
| 75 |
+
if (!this.wsBadge) return;
|
| 76 |
+
const state = status === 'connected' ? 'ok' : status === 'connecting' ? 'warn' : 'error';
|
| 77 |
+
this.wsBadge.dataset.state = state;
|
| 78 |
+
const textNode = this.wsBadge.querySelectorAll('span')[1];
|
| 79 |
+
if (textNode) textNode.textContent = status;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
});
|
| 81 |
},
|
| 82 |
|
| 83 |
+
async refreshHealth() {
|
| 84 |
+
if (!this.apiHealthBadge) return;
|
| 85 |
+
const result = await apiClient.getHealth();
|
| 86 |
+
if (result.ok) {
|
| 87 |
+
this.apiHealthBadge.dataset.state = 'ok';
|
| 88 |
+
const textNode = this.apiHealthBadge.querySelectorAll('span')[1];
|
| 89 |
+
if (textNode) textNode.textContent = result.data?.status || 'healthy';
|
| 90 |
+
} else {
|
| 91 |
+
this.apiHealthBadge.dataset.state = 'error';
|
| 92 |
+
const textNode = this.apiHealthBadge.querySelectorAll('span')[1];
|
| 93 |
+
if (textNode) textNode.textContent = 'error';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
};
|
| 97 |
|
| 98 |
+
window.addEventListener('DOMContentLoaded', () => App.init());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static/js/chartLabView.js
CHANGED
|
@@ -1,458 +1,127 @@
|
|
| 1 |
import apiClient from './apiClient.js';
|
| 2 |
-
import errorHelper from './errorHelper.js';
|
| 3 |
-
import { createAdvancedLineChart, createCandlestickChart, createVolumeChart } from './tradingview-charts.js';
|
| 4 |
-
|
| 5 |
-
// Cryptocurrency symbols list
|
| 6 |
-
const CRYPTO_SYMBOLS = [
|
| 7 |
-
{ symbol: 'BTC', name: 'Bitcoin' },
|
| 8 |
-
{ symbol: 'ETH', name: 'Ethereum' },
|
| 9 |
-
{ symbol: 'BNB', name: 'Binance Coin' },
|
| 10 |
-
{ symbol: 'SOL', name: 'Solana' },
|
| 11 |
-
{ symbol: 'XRP', name: 'Ripple' },
|
| 12 |
-
{ symbol: 'ADA', name: 'Cardano' },
|
| 13 |
-
{ symbol: 'DOGE', name: 'Dogecoin' },
|
| 14 |
-
{ symbol: 'DOT', name: 'Polkadot' },
|
| 15 |
-
{ symbol: 'MATIC', name: 'Polygon' },
|
| 16 |
-
{ symbol: 'AVAX', name: 'Avalanche' },
|
| 17 |
-
{ symbol: 'LINK', name: 'Chainlink' },
|
| 18 |
-
{ symbol: 'UNI', name: 'Uniswap' },
|
| 19 |
-
{ symbol: 'LTC', name: 'Litecoin' },
|
| 20 |
-
{ symbol: 'ATOM', name: 'Cosmos' },
|
| 21 |
-
{ symbol: 'ALGO', name: 'Algorand' },
|
| 22 |
-
{ symbol: 'TRX', name: 'Tron' },
|
| 23 |
-
{ symbol: 'XLM', name: 'Stellar' },
|
| 24 |
-
{ symbol: 'VET', name: 'VeChain' },
|
| 25 |
-
{ symbol: 'FIL', name: 'Filecoin' },
|
| 26 |
-
{ symbol: 'ETC', name: 'Ethereum Classic' },
|
| 27 |
-
{ symbol: 'AAVE', name: 'Aave' },
|
| 28 |
-
{ symbol: 'MKR', name: 'Maker' },
|
| 29 |
-
{ symbol: 'COMP', name: 'Compound' },
|
| 30 |
-
{ symbol: 'SUSHI', name: 'SushiSwap' },
|
| 31 |
-
{ symbol: 'YFI', name: 'Yearn Finance' },
|
| 32 |
-
];
|
| 33 |
|
| 34 |
class ChartLabView {
|
| 35 |
constructor(section) {
|
| 36 |
this.section = section;
|
| 37 |
-
this.
|
| 38 |
-
this.
|
| 39 |
-
this.
|
| 40 |
-
this.
|
| 41 |
-
this.
|
| 42 |
-
this.
|
| 43 |
-
this.runAnalysisButton = section.querySelector('[data-run-analysis]');
|
| 44 |
-
this.canvas = section.querySelector('#price-chart');
|
| 45 |
-
this.analysisOutput = section.querySelector('[data-analysis-output]');
|
| 46 |
-
this.chartTitle = section.querySelector('[data-chart-title]');
|
| 47 |
-
this.chartLegend = section.querySelector('[data-chart-legend]');
|
| 48 |
this.chart = null;
|
| 49 |
this.symbol = 'BTC';
|
| 50 |
this.timeframe = '7d';
|
| 51 |
-
this.filteredSymbols = [...CRYPTO_SYMBOLS];
|
| 52 |
}
|
| 53 |
|
| 54 |
async init() {
|
| 55 |
-
this.setupCombobox();
|
| 56 |
-
this.bindEvents();
|
| 57 |
await this.loadChart();
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
setupCombobox() {
|
| 61 |
-
if (!this.symbolInput || !this.symbolOptions) return;
|
| 62 |
-
|
| 63 |
-
// Populate options
|
| 64 |
-
this.renderOptions();
|
| 65 |
-
|
| 66 |
-
// Set initial value
|
| 67 |
-
this.symbolInput.value = 'BTC - Bitcoin';
|
| 68 |
-
|
| 69 |
-
// Input event for filtering
|
| 70 |
-
this.symbolInput.addEventListener('input', (e) => {
|
| 71 |
-
const query = e.target.value.trim().toUpperCase();
|
| 72 |
-
this.filterSymbols(query);
|
| 73 |
-
});
|
| 74 |
-
|
| 75 |
-
// Focus event to show dropdown
|
| 76 |
-
this.symbolInput.addEventListener('focus', () => {
|
| 77 |
-
this.symbolDropdown.style.display = 'block';
|
| 78 |
-
this.filterSymbols(this.symbolInput.value.trim().toUpperCase());
|
| 79 |
-
});
|
| 80 |
-
|
| 81 |
-
// Click outside to close
|
| 82 |
-
document.addEventListener('click', (e) => {
|
| 83 |
-
if (!this.symbolInput.contains(e.target) && !this.symbolDropdown.contains(e.target)) {
|
| 84 |
-
this.symbolDropdown.style.display = 'none';
|
| 85 |
-
}
|
| 86 |
-
});
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
filterSymbols(query) {
|
| 90 |
-
if (!query) {
|
| 91 |
-
this.filteredSymbols = [...CRYPTO_SYMBOLS];
|
| 92 |
-
} else {
|
| 93 |
-
this.filteredSymbols = CRYPTO_SYMBOLS.filter(item =>
|
| 94 |
-
item.symbol.includes(query) ||
|
| 95 |
-
item.name.toUpperCase().includes(query)
|
| 96 |
-
);
|
| 97 |
-
}
|
| 98 |
-
this.renderOptions();
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
renderOptions() {
|
| 102 |
-
if (!this.symbolOptions) return;
|
| 103 |
-
|
| 104 |
-
if (this.filteredSymbols.length === 0) {
|
| 105 |
-
this.symbolOptions.innerHTML = '<div class="combobox-option disabled">No results found</div>';
|
| 106 |
-
return;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
this.symbolOptions.innerHTML = this.filteredSymbols.map(item => `
|
| 110 |
-
<div class="combobox-option" data-symbol="${item.symbol}">
|
| 111 |
-
<strong>${item.symbol}</strong>
|
| 112 |
-
<span>${item.name}</span>
|
| 113 |
-
</div>
|
| 114 |
-
`).join('');
|
| 115 |
-
|
| 116 |
-
// Add click handlers
|
| 117 |
-
this.symbolOptions.querySelectorAll('.combobox-option').forEach(option => {
|
| 118 |
-
if (!option.classList.contains('disabled')) {
|
| 119 |
-
option.addEventListener('click', () => {
|
| 120 |
-
const symbol = option.dataset.symbol;
|
| 121 |
-
const item = CRYPTO_SYMBOLS.find(i => i.symbol === symbol);
|
| 122 |
-
if (item) {
|
| 123 |
-
this.symbol = symbol;
|
| 124 |
-
this.symbolInput.value = `${item.symbol} - ${item.name}`;
|
| 125 |
-
this.symbolDropdown.style.display = 'none';
|
| 126 |
-
this.loadChart();
|
| 127 |
-
}
|
| 128 |
-
});
|
| 129 |
-
}
|
| 130 |
-
});
|
| 131 |
}
|
| 132 |
|
| 133 |
bindEvents() {
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
this.timeframeButtons.forEach((btn) => {
|
| 136 |
btn.addEventListener('click', async () => {
|
| 137 |
this.timeframeButtons.forEach((b) => b.classList.remove('active'));
|
| 138 |
btn.classList.add('active');
|
| 139 |
-
this.timeframe = btn.dataset.
|
| 140 |
await this.loadChart();
|
| 141 |
});
|
| 142 |
});
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if (this.loadButton) {
|
| 146 |
-
this.loadButton.addEventListener('click', async (e) => {
|
| 147 |
-
e.preventDefault();
|
| 148 |
-
// Extract symbol from input
|
| 149 |
-
const inputValue = this.symbolInput.value.trim();
|
| 150 |
-
if (inputValue) {
|
| 151 |
-
const match = inputValue.match(/^([A-Z0-9]+)/);
|
| 152 |
-
if (match) {
|
| 153 |
-
this.symbol = match[1].toUpperCase();
|
| 154 |
-
} else {
|
| 155 |
-
this.symbol = inputValue.toUpperCase();
|
| 156 |
-
}
|
| 157 |
-
}
|
| 158 |
-
await this.loadChart();
|
| 159 |
-
});
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
// Indicator buttons
|
| 163 |
-
if (this.indicatorButtons.length > 0) {
|
| 164 |
-
this.indicatorButtons.forEach((btn) => {
|
| 165 |
-
btn.addEventListener('click', () => {
|
| 166 |
-
btn.classList.toggle('active');
|
| 167 |
-
// Don't auto-run, wait for Run Analysis button
|
| 168 |
-
});
|
| 169 |
-
});
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
// Run analysis button
|
| 173 |
-
if (this.runAnalysisButton) {
|
| 174 |
-
this.runAnalysisButton.addEventListener('click', async (e) => {
|
| 175 |
-
e.preventDefault();
|
| 176 |
-
await this.runAnalysis();
|
| 177 |
-
});
|
| 178 |
}
|
| 179 |
}
|
| 180 |
|
| 181 |
async loadChart() {
|
| 182 |
if (!this.canvas) return;
|
| 183 |
-
|
| 184 |
-
const
|
| 185 |
-
if (!
|
| 186 |
-
this.symbol = 'BTC';
|
| 187 |
-
if (this.symbolInput) this.symbolInput.value = 'BTC - Bitcoin';
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
const container = this.canvas.closest('.chart-wrapper') || this.canvas.parentElement;
|
| 191 |
-
|
| 192 |
-
// Show loading state
|
| 193 |
-
if (container) {
|
| 194 |
-
let loadingNode = container.querySelector('.chart-loading');
|
| 195 |
-
if (!loadingNode) {
|
| 196 |
-
loadingNode = document.createElement('div');
|
| 197 |
-
loadingNode.className = 'chart-loading';
|
| 198 |
-
container.insertBefore(loadingNode, this.canvas);
|
| 199 |
-
}
|
| 200 |
-
loadingNode.innerHTML = `
|
| 201 |
-
<div class="loading-spinner"></div>
|
| 202 |
-
<p>Loading ${symbol} chart data...</p>
|
| 203 |
-
`;
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
// Update title
|
| 207 |
-
if (this.chartTitle) {
|
| 208 |
-
this.chartTitle.textContent = `${symbol} Price Chart (${this.timeframe})`;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
try {
|
| 212 |
-
const result = await apiClient.getPriceChart(symbol, this.timeframe);
|
| 213 |
-
|
| 214 |
-
// Remove loading
|
| 215 |
-
if (container) {
|
| 216 |
-
const loadingNode = container.querySelector('.chart-loading');
|
| 217 |
-
if (loadingNode) loadingNode.remove();
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
if (!result.ok) {
|
| 221 |
-
const errorAnalysis = errorHelper.analyzeError(new Error(result.error), { symbol, timeframe: this.timeframe });
|
| 222 |
-
|
| 223 |
-
if (container) {
|
| 224 |
-
let errorNode = container.querySelector('.chart-error');
|
| 225 |
-
if (!errorNode) {
|
| 226 |
-
errorNode = document.createElement('div');
|
| 227 |
-
errorNode.className = 'inline-message inline-error chart-error';
|
| 228 |
-
container.appendChild(errorNode);
|
| 229 |
-
}
|
| 230 |
-
errorNode.innerHTML = `
|
| 231 |
-
<strong>Error loading chart:</strong>
|
| 232 |
-
<p>${result.error || 'Failed to load chart data'}</p>
|
| 233 |
-
<p><small>Symbol: ${symbol} | Timeframe: ${this.timeframe}</small></p>
|
| 234 |
-
`;
|
| 235 |
-
}
|
| 236 |
-
return;
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
if (container) {
|
| 240 |
-
|
| 241 |
-
if (errorNode)
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
// Parse chart data
|
| 245 |
-
const chartData = result.data || {};
|
| 246 |
-
const points = chartData.data || chartData || [];
|
| 247 |
-
|
| 248 |
-
if (!points || points.length === 0) {
|
| 249 |
-
if (container) {
|
| 250 |
-
const errorNode = document.createElement('div');
|
| 251 |
-
errorNode.className = 'inline-message inline-warn';
|
| 252 |
-
errorNode.innerHTML = '<strong>No data available</strong><p>No price data found for this symbol and timeframe.</p>';
|
| 253 |
container.appendChild(errorNode);
|
| 254 |
}
|
| 255 |
-
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
// Format labels and data
|
| 259 |
-
const labels = points.map((point) => {
|
| 260 |
-
const ts = point.time || point.timestamp || point.date;
|
| 261 |
-
if (!ts) return '';
|
| 262 |
-
const date = new Date(ts);
|
| 263 |
-
if (this.timeframe === '1d') {
|
| 264 |
-
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
| 265 |
-
}
|
| 266 |
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
| 267 |
-
});
|
| 268 |
-
|
| 269 |
-
const prices = points.map((point) => {
|
| 270 |
-
const price = point.price || point.close || point.value || 0;
|
| 271 |
-
return parseFloat(price) || 0;
|
| 272 |
-
});
|
| 273 |
-
|
| 274 |
-
// Destroy existing chart
|
| 275 |
-
if (this.chart) {
|
| 276 |
-
this.chart.destroy();
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
// Calculate min/max for better scaling
|
| 280 |
-
const minPrice = Math.min(...prices);
|
| 281 |
-
const maxPrice = Math.max(...prices);
|
| 282 |
-
const priceRange = maxPrice - minPrice;
|
| 283 |
-
const firstPrice = prices[0];
|
| 284 |
-
const lastPrice = prices[prices.length - 1];
|
| 285 |
-
const priceChange = lastPrice - firstPrice;
|
| 286 |
-
const priceChangePercent = ((priceChange / firstPrice) * 100).toFixed(2);
|
| 287 |
-
const isPriceUp = priceChange >= 0;
|
| 288 |
-
|
| 289 |
-
// Get indicator states
|
| 290 |
-
const showMA20 = this.section.querySelector('[data-indicator="MA20"]')?.checked || false;
|
| 291 |
-
const showMA50 = this.section.querySelector('[data-indicator="MA50"]')?.checked || false;
|
| 292 |
-
const showRSI = this.section.querySelector('[data-indicator="RSI"]')?.checked || false;
|
| 293 |
-
const showVolume = this.section.querySelector('[data-indicator="Volume"]')?.checked || false;
|
| 294 |
-
|
| 295 |
-
// Prepare price data for TradingView chart
|
| 296 |
-
const priceData = points.map((point, index) => ({
|
| 297 |
-
time: point.time || point.timestamp || point.date || new Date().getTime() + (index * 60000),
|
| 298 |
-
price: parseFloat(point.price || point.close || point.value || 0),
|
| 299 |
-
volume: parseFloat(point.volume || 0)
|
| 300 |
-
}));
|
| 301 |
-
|
| 302 |
-
// Create TradingView-style chart with indicators
|
| 303 |
-
this.chart = createAdvancedLineChart('chart-lab-canvas', priceData, {
|
| 304 |
-
showMA20,
|
| 305 |
-
showMA50,
|
| 306 |
-
showRSI,
|
| 307 |
-
showVolume
|
| 308 |
-
});
|
| 309 |
-
|
| 310 |
-
// If volume is enabled, create separate volume chart
|
| 311 |
-
if (showVolume && priceData.some(p => p.volume > 0)) {
|
| 312 |
-
const volumeContainer = this.section.querySelector('[data-volume-chart]');
|
| 313 |
-
if (volumeContainer) {
|
| 314 |
-
createVolumeChart('volume-chart-canvas', priceData);
|
| 315 |
-
}
|
| 316 |
-
}
|
| 317 |
-
|
| 318 |
-
// Update legend with TradingView-style info
|
| 319 |
-
if (this.chartLegend && prices.length > 0) {
|
| 320 |
-
const currentPrice = prices[prices.length - 1];
|
| 321 |
-
const firstPrice = prices[0];
|
| 322 |
-
const change = currentPrice - firstPrice;
|
| 323 |
-
const changePercent = ((change / firstPrice) * 100).toFixed(2);
|
| 324 |
-
const isUp = change >= 0;
|
| 325 |
-
|
| 326 |
-
this.chartLegend.innerHTML = `
|
| 327 |
-
<div class="legend-item">
|
| 328 |
-
<span class="legend-label">Price</span>
|
| 329 |
-
<span class="legend-value">$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
|
| 330 |
-
</div>
|
| 331 |
-
<div class="legend-item">
|
| 332 |
-
<span class="legend-label">24h</span>
|
| 333 |
-
<span class="legend-value ${isUp ? 'positive' : 'negative'}">
|
| 334 |
-
<span class="legend-arrow">${isUp ? '↑' : '↓'}</span>
|
| 335 |
-
${isUp ? '+' : ''}${changePercent}%
|
| 336 |
-
</span>
|
| 337 |
-
</div>
|
| 338 |
-
<div class="legend-item">
|
| 339 |
-
<span class="legend-label">High</span>
|
| 340 |
-
<span class="legend-value">$${maxPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
|
| 341 |
-
</div>
|
| 342 |
-
<div class="legend-item">
|
| 343 |
-
<span class="legend-label">Low</span>
|
| 344 |
-
<span class="legend-value">$${minPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
|
| 345 |
-
</div>
|
| 346 |
-
`;
|
| 347 |
-
}
|
| 348 |
-
} catch (error) {
|
| 349 |
-
console.error('Chart loading error:', error);
|
| 350 |
-
if (container) {
|
| 351 |
-
const errorNode = document.createElement('div');
|
| 352 |
-
errorNode.className = 'inline-message inline-error';
|
| 353 |
-
errorNode.innerHTML = `<strong>Error:</strong><p>${error.message || 'Failed to load chart'}</p>`;
|
| 354 |
-
container.appendChild(errorNode);
|
| 355 |
}
|
|
|
|
| 356 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
|
| 359 |
async runAnalysis() {
|
| 360 |
-
if (!this.
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
.
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
<p>Running AI analysis with ${enabledIndicators.length > 0 ? enabledIndicators.join(', ') : 'default'} indicators...</p>
|
| 370 |
-
</div>
|
| 371 |
-
`;
|
| 372 |
-
|
| 373 |
-
try {
|
| 374 |
-
const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators);
|
| 375 |
-
|
| 376 |
-
if (!result.ok) {
|
| 377 |
-
this.analysisOutput.innerHTML = `
|
| 378 |
-
<div class="inline-message inline-error">
|
| 379 |
-
<strong>Analysis Error:</strong>
|
| 380 |
-
<p>${result.error || 'Failed to run analysis'}</p>
|
| 381 |
-
</div>
|
| 382 |
-
`;
|
| 383 |
-
return;
|
| 384 |
-
}
|
| 385 |
-
|
| 386 |
-
const data = result.data || {};
|
| 387 |
-
const analysis = data.analysis || data;
|
| 388 |
-
|
| 389 |
-
if (!analysis) {
|
| 390 |
-
this.analysisOutput.innerHTML = '<div class="inline-message inline-warn">No AI insights returned.</div>';
|
| 391 |
-
return;
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
const summary = analysis.summary || analysis.narrative?.summary || 'No summary available.';
|
| 395 |
-
const signals = analysis.signals || {};
|
| 396 |
-
const direction = analysis.change_direction || 'N/A';
|
| 397 |
-
const changePercent = analysis.change_percent ?? '—';
|
| 398 |
-
const high = analysis.high ?? '—';
|
| 399 |
-
const low = analysis.low ?? '—';
|
| 400 |
-
|
| 401 |
-
const bullets = Object.entries(signals)
|
| 402 |
-
.map(([key, value]) => {
|
| 403 |
-
const label = value?.label || value || 'n/a';
|
| 404 |
-
const score = value?.score ?? value?.value ?? '—';
|
| 405 |
-
return `<li><strong>${key.toUpperCase()}:</strong> ${label} ${score !== '—' ? `(${score})` : ''}</li>`;
|
| 406 |
-
})
|
| 407 |
-
.join('');
|
| 408 |
-
|
| 409 |
-
this.analysisOutput.innerHTML = `
|
| 410 |
-
<div class="analysis-results">
|
| 411 |
-
<div class="analysis-header">
|
| 412 |
-
<h5>Analysis Results</h5>
|
| 413 |
-
<span class="analysis-badge ${direction.toLowerCase()}">${direction}</span>
|
| 414 |
-
</div>
|
| 415 |
-
<div class="analysis-metrics">
|
| 416 |
-
<div class="metric-item">
|
| 417 |
-
<span class="metric-label">Direction</span>
|
| 418 |
-
<span class="metric-value ${direction.toLowerCase()}">${direction}</span>
|
| 419 |
-
</div>
|
| 420 |
-
<div class="metric-item">
|
| 421 |
-
<span class="metric-label">Change</span>
|
| 422 |
-
<span class="metric-value ${changePercent >= 0 ? 'positive' : 'negative'}">
|
| 423 |
-
${changePercent >= 0 ? '+' : ''}${changePercent}%
|
| 424 |
-
</span>
|
| 425 |
-
</div>
|
| 426 |
-
<div class="metric-item">
|
| 427 |
-
<span class="metric-label">High</span>
|
| 428 |
-
<span class="metric-value">$${high}</span>
|
| 429 |
-
</div>
|
| 430 |
-
<div class="metric-item">
|
| 431 |
-
<span class="metric-label">Low</span>
|
| 432 |
-
<span class="metric-value">$${low}</span>
|
| 433 |
-
</div>
|
| 434 |
-
</div>
|
| 435 |
-
<div class="analysis-summary">
|
| 436 |
-
<h6>Summary</h6>
|
| 437 |
-
<p>${summary}</p>
|
| 438 |
-
</div>
|
| 439 |
-
${bullets ? `
|
| 440 |
-
<div class="analysis-signals">
|
| 441 |
-
<h6>Signals</h6>
|
| 442 |
-
<ul>${bullets}</ul>
|
| 443 |
-
</div>
|
| 444 |
-
` : ''}
|
| 445 |
-
</div>
|
| 446 |
-
`;
|
| 447 |
-
} catch (error) {
|
| 448 |
-
console.error('Analysis error:', error);
|
| 449 |
-
this.analysisOutput.innerHTML = `
|
| 450 |
-
<div class="inline-message inline-error">
|
| 451 |
-
<strong>Error:</strong>
|
| 452 |
-
<p>${error.message || 'Failed to run analysis'}</p>
|
| 453 |
-
</div>
|
| 454 |
-
`;
|
| 455 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
}
|
| 457 |
}
|
| 458 |
|
|
|
|
| 1 |
import apiClient from './apiClient.js';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
class ChartLabView {
|
| 4 |
constructor(section) {
|
| 5 |
this.section = section;
|
| 6 |
+
this.symbolSelect = section.querySelector('[data-chart-symbol]');
|
| 7 |
+
this.timeframeButtons = section.querySelectorAll('[data-chart-timeframe]');
|
| 8 |
+
this.indicatorInputs = section.querySelectorAll('[data-indicator]');
|
| 9 |
+
this.analyzeButton = section.querySelector('[data-run-analysis]');
|
| 10 |
+
this.canvas = section.querySelector('#chart-lab-canvas');
|
| 11 |
+
this.insightsContainer = section.querySelector('[data-ai-insights]');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
this.chart = null;
|
| 13 |
this.symbol = 'BTC';
|
| 14 |
this.timeframe = '7d';
|
|
|
|
| 15 |
}
|
| 16 |
|
| 17 |
async init() {
|
|
|
|
|
|
|
| 18 |
await this.loadChart();
|
| 19 |
+
this.bindEvents();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
}
|
| 21 |
|
| 22 |
bindEvents() {
|
| 23 |
+
if (this.symbolSelect) {
|
| 24 |
+
this.symbolSelect.addEventListener('change', async () => {
|
| 25 |
+
this.symbol = this.symbolSelect.value;
|
| 26 |
+
await this.loadChart();
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
this.timeframeButtons.forEach((btn) => {
|
| 30 |
btn.addEventListener('click', async () => {
|
| 31 |
this.timeframeButtons.forEach((b) => b.classList.remove('active'));
|
| 32 |
btn.classList.add('active');
|
| 33 |
+
this.timeframe = btn.dataset.chartTimeframe;
|
| 34 |
await this.loadChart();
|
| 35 |
});
|
| 36 |
});
|
| 37 |
+
if (this.analyzeButton) {
|
| 38 |
+
this.analyzeButton.addEventListener('click', () => this.runAnalysis());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
}
|
| 41 |
|
| 42 |
async loadChart() {
|
| 43 |
if (!this.canvas) return;
|
| 44 |
+
const result = await apiClient.getPriceChart(this.symbol, this.timeframe);
|
| 45 |
+
const container = this.canvas.parentElement;
|
| 46 |
+
if (!result.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
if (container) {
|
| 48 |
+
let errorNode = container.querySelector('.chart-error');
|
| 49 |
+
if (!errorNode) {
|
| 50 |
+
errorNode = document.createElement('div');
|
| 51 |
+
errorNode.className = 'inline-message inline-error chart-error';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
container.appendChild(errorNode);
|
| 53 |
}
|
| 54 |
+
errorNode.textContent = result.error;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
}
|
| 56 |
+
return;
|
| 57 |
}
|
| 58 |
+
if (container) {
|
| 59 |
+
const errorNode = container.querySelector('.chart-error');
|
| 60 |
+
if (errorNode) errorNode.remove();
|
| 61 |
+
}
|
| 62 |
+
const points = result.data || [];
|
| 63 |
+
const labels = points.map((point) => point.time || point.timestamp || '');
|
| 64 |
+
const prices = points.map((point) => point.price || point.close || point.value);
|
| 65 |
+
if (this.chart) {
|
| 66 |
+
this.chart.destroy();
|
| 67 |
+
}
|
| 68 |
+
this.chart = new Chart(this.canvas, {
|
| 69 |
+
type: 'line',
|
| 70 |
+
data: {
|
| 71 |
+
labels,
|
| 72 |
+
datasets: [
|
| 73 |
+
{
|
| 74 |
+
label: `${this.symbol} (${this.timeframe})`,
|
| 75 |
+
data: prices,
|
| 76 |
+
borderColor: '#f472b6',
|
| 77 |
+
backgroundColor: 'rgba(244, 114, 182, 0.2)',
|
| 78 |
+
fill: true,
|
| 79 |
+
tension: 0.4,
|
| 80 |
+
},
|
| 81 |
+
],
|
| 82 |
+
},
|
| 83 |
+
options: {
|
| 84 |
+
scales: {
|
| 85 |
+
x: { ticks: { color: 'var(--text-muted)' } },
|
| 86 |
+
y: { ticks: { color: 'var(--text-muted)' } },
|
| 87 |
+
},
|
| 88 |
+
plugins: {
|
| 89 |
+
legend: { display: false },
|
| 90 |
+
},
|
| 91 |
+
},
|
| 92 |
+
});
|
| 93 |
}
|
| 94 |
|
| 95 |
async runAnalysis() {
|
| 96 |
+
if (!this.insightsContainer) return;
|
| 97 |
+
const enabledIndicators = Array.from(this.indicatorInputs)
|
| 98 |
+
.filter((input) => input.checked)
|
| 99 |
+
.map((input) => input.value);
|
| 100 |
+
this.insightsContainer.innerHTML = '<p>Running AI analysis...</p>';
|
| 101 |
+
const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators);
|
| 102 |
+
if (!result.ok) {
|
| 103 |
+
this.insightsContainer.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
|
| 104 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
+
const payload = result.data || {};
|
| 107 |
+
const insights = payload.insights || result.insights || payload;
|
| 108 |
+
if (!insights) {
|
| 109 |
+
this.insightsContainer.innerHTML = '<p>No AI insights returned.</p>';
|
| 110 |
+
return;
|
| 111 |
+
}
|
| 112 |
+
const summary =
|
| 113 |
+
insights.narrative?.summary?.summary || insights.narrative?.summary || insights.narrative?.summary_text;
|
| 114 |
+
const signals = insights.narrative?.signals || {};
|
| 115 |
+
const bullets = Object.entries(signals)
|
| 116 |
+
.map(([key, value]) => `<li><strong>${key}:</strong> ${(value?.label || 'n/a')} (${value?.score ?? '—'})</li>`)
|
| 117 |
+
.join('');
|
| 118 |
+
this.insightsContainer.innerHTML = `
|
| 119 |
+
<h4>AI Insights</h4>
|
| 120 |
+
<p><strong>Direction:</strong> ${insights.change_direction || 'N/A'} (${insights.change_percent ?? '—'}%)</p>
|
| 121 |
+
<p><strong>Range:</strong> High ${insights.high ?? '—'} / Low ${insights.low ?? '—'}</p>
|
| 122 |
+
<p>${summary || insights.narrative?.summary?.summary || insights.narrative?.summary || ''}</p>
|
| 123 |
+
<ul>${bullets || '<li>No sentiment signals provided.</li>'}</ul>
|
| 124 |
+
`;
|
| 125 |
}
|
| 126 |
}
|
| 127 |
|
static/js/datasetsModelsView.js
CHANGED
|
@@ -54,9 +54,7 @@ class DatasetsModelsView {
|
|
| 54 |
this.datasetsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
|
| 55 |
return;
|
| 56 |
}
|
| 57 |
-
|
| 58 |
-
const data = result.data || {};
|
| 59 |
-
this.datasets = data.datasets || data || [];
|
| 60 |
this.datasetsBody.innerHTML = this.datasets
|
| 61 |
.map(
|
| 62 |
(dataset) => `
|
|
@@ -83,9 +81,7 @@ class DatasetsModelsView {
|
|
| 83 |
this.previewContent.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
|
| 84 |
return;
|
| 85 |
}
|
| 86 |
-
|
| 87 |
-
const data = result.data || {};
|
| 88 |
-
const rows = data.sample || data || [];
|
| 89 |
if (!rows.length) {
|
| 90 |
this.previewContent.innerHTML = '<p>No sample rows available.</p>';
|
| 91 |
return;
|
|
@@ -115,9 +111,7 @@ class DatasetsModelsView {
|
|
| 115 |
this.modelsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
|
| 116 |
return;
|
| 117 |
}
|
| 118 |
-
|
| 119 |
-
const data = result.data || {};
|
| 120 |
-
this.models = data.models || data || [];
|
| 121 |
this.modelsBody.innerHTML = this.models
|
| 122 |
.map(
|
| 123 |
(model) => `
|
|
|
|
| 54 |
this.datasetsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
|
| 55 |
return;
|
| 56 |
}
|
| 57 |
+
this.datasets = result.data || [];
|
|
|
|
|
|
|
| 58 |
this.datasetsBody.innerHTML = this.datasets
|
| 59 |
.map(
|
| 60 |
(dataset) => `
|
|
|
|
| 81 |
this.previewContent.innerHTML = `<div class="inline-message inline-error">${result.error}</div>`;
|
| 82 |
return;
|
| 83 |
}
|
| 84 |
+
const rows = result.data || [];
|
|
|
|
|
|
|
| 85 |
if (!rows.length) {
|
| 86 |
this.previewContent.innerHTML = '<p>No sample rows available.</p>';
|
| 87 |
return;
|
|
|
|
| 111 |
this.modelsBody.innerHTML = `<tr><td colspan="4">${result.error}</td></tr>`;
|
| 112 |
return;
|
| 113 |
}
|
| 114 |
+
this.models = result.data || [];
|
|
|
|
|
|
|
| 115 |
this.modelsBody.innerHTML = this.models
|
| 116 |
.map(
|
| 117 |
(model) => `
|
static/js/debugConsoleView.js
CHANGED
|
@@ -4,8 +4,8 @@ class DebugConsoleView {
|
|
| 4 |
constructor(section, wsClient) {
|
| 5 |
this.section = section;
|
| 6 |
this.wsClient = wsClient;
|
| 7 |
-
this.
|
| 8 |
-
this.
|
| 9 |
this.requestLogBody = section.querySelector('[data-request-log]');
|
| 10 |
this.errorLogBody = section.querySelector('[data-error-log]');
|
| 11 |
this.wsLogBody = section.querySelector('[data-ws-log]');
|
|
@@ -25,31 +25,29 @@ class DebugConsoleView {
|
|
| 25 |
|
| 26 |
async refresh() {
|
| 27 |
const [health, providers] = await Promise.all([apiClient.getHealth(), apiClient.getProviders()]);
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
const data = health.data || {};
|
| 33 |
-
this.healthInfo.innerHTML = `
|
| 34 |
-
<p><strong>Status:</strong> <span class="text-success">${data.status || 'OK'}</span></p>
|
| 35 |
-
<p><strong>Uptime:</strong> ${data.uptime || 'N/A'}</p>
|
| 36 |
-
<p><strong>Version:</strong> ${data.version || 'N/A'}</p>
|
| 37 |
-
`;
|
| 38 |
-
} else {
|
| 39 |
-
this.healthInfo.innerHTML = `<div class="inline-message inline-error">${health.error || 'Unavailable'}</div>`;
|
| 40 |
-
}
|
| 41 |
}
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
-
|
| 53 |
this.renderRequestLogs();
|
| 54 |
this.renderErrorLogs();
|
| 55 |
this.renderWsLogs();
|
|
|
|
| 4 |
constructor(section, wsClient) {
|
| 5 |
this.section = section;
|
| 6 |
this.wsClient = wsClient;
|
| 7 |
+
this.healthStatus = section.querySelector('[data-health-status]');
|
| 8 |
+
this.providersContainer = section.querySelector('[data-providers]');
|
| 9 |
this.requestLogBody = section.querySelector('[data-request-log]');
|
| 10 |
this.errorLogBody = section.querySelector('[data-error-log]');
|
| 11 |
this.wsLogBody = section.querySelector('[data-ws-log]');
|
|
|
|
| 25 |
|
| 26 |
async refresh() {
|
| 27 |
const [health, providers] = await Promise.all([apiClient.getHealth(), apiClient.getProviders()]);
|
| 28 |
+
if (health.ok) {
|
| 29 |
+
this.healthStatus.textContent = health.data?.status || 'OK';
|
| 30 |
+
} else {
|
| 31 |
+
this.healthStatus.textContent = 'Unavailable';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
+
if (providers.ok) {
|
| 34 |
+
const list = providers.data || [];
|
| 35 |
+
this.providersContainer.innerHTML = list
|
| 36 |
+
.map(
|
| 37 |
+
(provider) => `
|
| 38 |
+
<div class="glass-card">
|
| 39 |
+
<h4>${provider.name}</h4>
|
| 40 |
+
<p>Status: <span class="${provider.status === 'healthy' ? 'text-success' : 'text-danger'}">${
|
| 41 |
+
provider.status || 'unknown'
|
| 42 |
+
}</span></p>
|
| 43 |
+
<p>Latency: ${provider.latency || '—'}ms</p>
|
| 44 |
+
</div>
|
| 45 |
+
`,
|
| 46 |
+
)
|
| 47 |
+
.join('');
|
| 48 |
+
} else {
|
| 49 |
+
this.providersContainer.innerHTML = `<div class="inline-message inline-error">${providers.error}</div>`;
|
| 50 |
}
|
|
|
|
| 51 |
this.renderRequestLogs();
|
| 52 |
this.renderErrorLogs();
|
| 53 |
this.renderWsLogs();
|
static/js/marketView.js
CHANGED
|
@@ -70,9 +70,7 @@ class MarketView {
|
|
| 70 |
</td></tr>`;
|
| 71 |
return;
|
| 72 |
}
|
| 73 |
-
|
| 74 |
-
const data = result.data || {};
|
| 75 |
-
this.coins = data.coins || data || [];
|
| 76 |
this.filtered = [...this.coins];
|
| 77 |
this.renderTable();
|
| 78 |
}
|
|
@@ -97,15 +95,7 @@ class MarketView {
|
|
| 97 |
</td>
|
| 98 |
<td>${coin.name || 'Unknown'}</td>
|
| 99 |
<td>${formatCurrency(coin.price)}</td>
|
| 100 |
-
<td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">
|
| 101 |
-
<span class="table-change-icon ${coin.change_24h >= 0 ? 'positive' : 'negative'}">
|
| 102 |
-
${coin.change_24h >= 0 ?
|
| 103 |
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
|
| 104 |
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
| 105 |
-
}
|
| 106 |
-
</span>
|
| 107 |
-
${formatPercent(coin.change_24h)}
|
| 108 |
-
</td>
|
| 109 |
<td>${formatCurrency(coin.volume_24h)}</td>
|
| 110 |
<td>${formatCurrency(coin.market_cap)}</td>
|
| 111 |
</tr>
|
|
@@ -164,10 +154,7 @@ class MarketView {
|
|
| 164 |
this.chartWrapper.innerHTML = `<div class="inline-message inline-error">${chart.error}</div>`;
|
| 165 |
}
|
| 166 |
} else {
|
| 167 |
-
|
| 168 |
-
const chartData = chart.data || {};
|
| 169 |
-
const points = chartData.data || chartData || [];
|
| 170 |
-
this.renderChart(points);
|
| 171 |
}
|
| 172 |
}
|
| 173 |
|
|
|
|
| 70 |
</td></tr>`;
|
| 71 |
return;
|
| 72 |
}
|
| 73 |
+
this.coins = result.data || [];
|
|
|
|
|
|
|
| 74 |
this.filtered = [...this.coins];
|
| 75 |
this.renderTable();
|
| 76 |
}
|
|
|
|
| 95 |
</td>
|
| 96 |
<td>${coin.name || 'Unknown'}</td>
|
| 97 |
<td>${formatCurrency(coin.price)}</td>
|
| 98 |
+
<td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">${formatPercent(coin.change_24h)}</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
<td>${formatCurrency(coin.volume_24h)}</td>
|
| 100 |
<td>${formatCurrency(coin.market_cap)}</td>
|
| 101 |
</tr>
|
|
|
|
| 154 |
this.chartWrapper.innerHTML = `<div class="inline-message inline-error">${chart.error}</div>`;
|
| 155 |
}
|
| 156 |
} else {
|
| 157 |
+
this.renderChart(chart.data || []);
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
}
|
| 160 |
|
static/js/newsView.js
CHANGED
|
@@ -48,9 +48,7 @@ class NewsView {
|
|
| 48 |
this.tableBody.innerHTML = `<tr><td colspan="6"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
|
| 49 |
return;
|
| 50 |
}
|
| 51 |
-
|
| 52 |
-
const data = result.data || {};
|
| 53 |
-
this.dataset = data.news || data || [];
|
| 54 |
this.datasetMap.clear();
|
| 55 |
this.dataset.forEach((item, index) => {
|
| 56 |
const rowId = item.id || `${item.title}-${index}`;
|
|
|
|
| 48 |
this.tableBody.innerHTML = `<tr><td colspan="6"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
|
| 49 |
return;
|
| 50 |
}
|
| 51 |
+
this.dataset = result.data || [];
|
|
|
|
|
|
|
| 52 |
this.datasetMap.clear();
|
| 53 |
this.dataset.forEach((item, index) => {
|
| 54 |
const rowId = item.id || `${item.title}-${index}`;
|
static/js/overviewView.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import apiClient from './apiClient.js';
|
| 2 |
import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js';
|
| 3 |
-
import { initMarketOverviewChart, createSparkline } from './charts-enhanced.js';
|
| 4 |
|
| 5 |
class OverviewView {
|
| 6 |
constructor(section) {
|
|
@@ -8,35 +7,13 @@ class OverviewView {
|
|
| 8 |
this.statsContainer = section.querySelector('[data-overview-stats]');
|
| 9 |
this.topCoinsBody = section.querySelector('[data-top-coins-body]');
|
| 10 |
this.sentimentCanvas = section.querySelector('#sentiment-chart');
|
| 11 |
-
this.marketOverviewCanvas = section.querySelector('#market-overview-chart');
|
| 12 |
this.sentimentChart = null;
|
| 13 |
-
this.marketData = [];
|
| 14 |
}
|
| 15 |
|
| 16 |
async init() {
|
| 17 |
this.renderStatSkeletons();
|
| 18 |
-
this.topCoinsBody.innerHTML = createSkeletonRows(6,
|
| 19 |
-
await Promise.all([
|
| 20 |
-
this.loadStats(),
|
| 21 |
-
this.loadTopCoins(),
|
| 22 |
-
this.loadSentiment(),
|
| 23 |
-
this.loadMarketOverview(),
|
| 24 |
-
this.loadBackendInfo()
|
| 25 |
-
]);
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
async loadMarketOverview() {
|
| 29 |
-
try {
|
| 30 |
-
const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
|
| 31 |
-
const data = await response.json();
|
| 32 |
-
this.marketData = data;
|
| 33 |
-
|
| 34 |
-
if (this.marketOverviewCanvas && data.length > 0) {
|
| 35 |
-
initMarketOverviewChart(data);
|
| 36 |
-
}
|
| 37 |
-
} catch (error) {
|
| 38 |
-
console.error('Error loading market overview:', error);
|
| 39 |
-
}
|
| 40 |
}
|
| 41 |
|
| 42 |
renderStatSkeletons() {
|
|
@@ -57,260 +34,60 @@ class OverviewView {
|
|
| 57 |
});
|
| 58 |
return;
|
| 59 |
}
|
| 60 |
-
|
| 61 |
-
const data = result.data || {};
|
| 62 |
-
const stats = data.stats || data;
|
| 63 |
-
|
| 64 |
-
// Debug: Log stats to see what we're getting
|
| 65 |
-
console.log('[OverviewView] Market Stats:', stats);
|
| 66 |
-
|
| 67 |
-
// Get change data from stats if available
|
| 68 |
-
const marketCapChange = stats.market_cap_change_24h || 0;
|
| 69 |
-
const volumeChange = stats.volume_change_24h || 0;
|
| 70 |
-
|
| 71 |
-
// Get Fear & Greed Index
|
| 72 |
-
const fearGreedValue = stats.fear_greed_value || stats.sentiment?.fear_greed_index?.value || stats.sentiment?.fear_greed_value || 50;
|
| 73 |
-
const fearGreedClassification = stats.sentiment?.fear_greed_index?.classification || stats.sentiment?.classification ||
|
| 74 |
-
(fearGreedValue >= 75 ? 'Extreme Greed' :
|
| 75 |
-
fearGreedValue >= 55 ? 'Greed' :
|
| 76 |
-
fearGreedValue >= 45 ? 'Neutral' :
|
| 77 |
-
fearGreedValue >= 25 ? 'Fear' : 'Extreme Fear');
|
| 78 |
-
|
| 79 |
const cards = [
|
| 80 |
-
{
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 85 |
-
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
| 86 |
-
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
| 87 |
-
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
| 88 |
-
</svg>`,
|
| 89 |
-
color: '#06B6D4'
|
| 90 |
-
},
|
| 91 |
-
{
|
| 92 |
-
label: '24h Volume',
|
| 93 |
-
value: formatCurrency(stats.total_volume_24h),
|
| 94 |
-
change: volumeChange,
|
| 95 |
-
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 96 |
-
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 97 |
-
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 98 |
-
</svg>`,
|
| 99 |
-
color: '#3B82F6'
|
| 100 |
-
},
|
| 101 |
-
{
|
| 102 |
-
label: 'BTC Dominance',
|
| 103 |
-
value: formatPercent(stats.btc_dominance),
|
| 104 |
-
change: (Math.random() * 0.5 - 0.25).toFixed(2),
|
| 105 |
-
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 106 |
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 107 |
-
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 108 |
-
</svg>`,
|
| 109 |
-
color: '#F97316'
|
| 110 |
-
},
|
| 111 |
-
{
|
| 112 |
-
label: 'Fear & Greed Index',
|
| 113 |
-
value: fearGreedValue,
|
| 114 |
-
change: null,
|
| 115 |
-
classification: fearGreedClassification,
|
| 116 |
-
icon: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 117 |
-
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor"/>
|
| 118 |
-
</svg>`,
|
| 119 |
-
color: fearGreedValue >= 75 ? '#EF4444' : fearGreedValue >= 55 ? '#F97316' : fearGreedValue >= 45 ? '#3B82F6' : fearGreedValue >= 25 ? '#8B5CF6' : '#6366F1',
|
| 120 |
-
isFearGreed: true
|
| 121 |
-
},
|
| 122 |
];
|
| 123 |
this.statsContainer.innerHTML = cards
|
| 124 |
.map(
|
| 125 |
-
(card) =>
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
if (card.isFearGreed) {
|
| 131 |
-
const fgColor = card.color;
|
| 132 |
-
const fgGradient = fearGreedValue >= 75 ? 'linear-gradient(135deg, #EF4444, #DC2626)' :
|
| 133 |
-
fearGreedValue >= 55 ? 'linear-gradient(135deg, #F97316, #EA580C)' :
|
| 134 |
-
fearGreedValue >= 45 ? 'linear-gradient(135deg, #3B82F6, #2563EB)' :
|
| 135 |
-
fearGreedValue >= 25 ? 'linear-gradient(135deg, #8B5CF6, #7C3AED)' :
|
| 136 |
-
'linear-gradient(135deg, #6366F1, #4F46E5)';
|
| 137 |
-
|
| 138 |
-
return `
|
| 139 |
-
<div class="glass-card stat-card fear-greed-card" style="--card-color: ${fgColor}">
|
| 140 |
-
<div class="stat-header">
|
| 141 |
-
<div class="stat-icon" style="color: ${fgColor}; background: ${fgGradient};">
|
| 142 |
-
${card.icon}
|
| 143 |
-
</div>
|
| 144 |
-
<h3>${card.label}</h3>
|
| 145 |
-
</div>
|
| 146 |
-
<div class="stat-value-wrapper">
|
| 147 |
-
<div class="stat-value fear-greed-value" style="color: ${fgColor}; font-size: 2.5rem; font-weight: 800;">
|
| 148 |
-
${card.value}
|
| 149 |
-
</div>
|
| 150 |
-
<div class="fear-greed-classification" style="color: ${fgColor}; font-weight: 600; font-size: 0.875rem; margin-top: 8px;">
|
| 151 |
-
${card.classification}
|
| 152 |
-
</div>
|
| 153 |
-
</div>
|
| 154 |
-
<div class="fear-greed-gauge" style="margin-top: 16px;">
|
| 155 |
-
<div class="gauge-bar" style="background: linear-gradient(90deg, #EF4444 0%, #F97316 25%, #3B82F6 50%, #8B5CF6 75%, #6366F1 100%); height: 8px; border-radius: 4px; position: relative; overflow: hidden;">
|
| 156 |
-
<div class="gauge-indicator" style="position: absolute; left: ${fearGreedValue}%; top: 50%; transform: translate(-50%, -50%); width: 16px; height: 16px; background: ${fgColor}; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 8px ${fgColor};"></div>
|
| 157 |
-
</div>
|
| 158 |
-
<div class="gauge-labels" style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 0.75rem; color: var(--text-muted);">
|
| 159 |
-
<span>Extreme Fear</span>
|
| 160 |
-
<span>Neutral</span>
|
| 161 |
-
<span>Extreme Greed</span>
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
<div class="stat-metrics">
|
| 165 |
-
<div class="stat-metric">
|
| 166 |
-
<span class="metric-label">Status</span>
|
| 167 |
-
<span class="metric-value" style="color: ${fgColor};">
|
| 168 |
-
${card.classification}
|
| 169 |
-
</span>
|
| 170 |
-
</div>
|
| 171 |
-
<div class="stat-metric">
|
| 172 |
-
<span class="metric-label">Updated</span>
|
| 173 |
-
<span class="metric-value">
|
| 174 |
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;">
|
| 175 |
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 176 |
-
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 177 |
-
</svg>
|
| 178 |
-
${new Date().toLocaleTimeString()}
|
| 179 |
-
</span>
|
| 180 |
-
</div>
|
| 181 |
-
</div>
|
| 182 |
-
</div>
|
| 183 |
-
`;
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
return `
|
| 187 |
-
<div class="glass-card stat-card" style="--card-color: ${card.color}">
|
| 188 |
-
<div class="stat-header">
|
| 189 |
-
<div class="stat-icon" style="color: ${card.color}">
|
| 190 |
-
${card.icon}
|
| 191 |
-
</div>
|
| 192 |
-
<h3>${card.label}</h3>
|
| 193 |
-
</div>
|
| 194 |
-
<div class="stat-value-wrapper">
|
| 195 |
-
<div class="stat-value">${card.value}</div>
|
| 196 |
-
${card.change !== null && card.change !== undefined ? `
|
| 197 |
-
<div class="stat-change ${isPositive ? 'positive' : 'negative'}">
|
| 198 |
-
<div class="change-icon-wrapper ${isPositive ? 'positive' : 'negative'}">
|
| 199 |
-
${isPositive ?
|
| 200 |
-
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
|
| 201 |
-
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
| 202 |
-
}
|
| 203 |
-
</div>
|
| 204 |
-
<span class="change-value">${isPositive ? '+' : ''}${changeValue.toFixed(2)}%</span>
|
| 205 |
-
</div>
|
| 206 |
-
` : ''}
|
| 207 |
-
</div>
|
| 208 |
-
<div class="stat-metrics">
|
| 209 |
-
<div class="stat-metric">
|
| 210 |
-
<span class="metric-label">24h Change</span>
|
| 211 |
-
<span class="metric-value ${card.change !== null && card.change !== undefined ? (isPositive ? 'positive' : 'negative') : ''}">
|
| 212 |
-
${card.change !== null && card.change !== undefined ? `
|
| 213 |
-
<span class="metric-icon ${isPositive ? 'positive' : 'negative'}">
|
| 214 |
-
${isPositive ? '↑' : '↓'}
|
| 215 |
-
</span>
|
| 216 |
-
${isPositive ? '+' : ''}${changeValue.toFixed(2)}%
|
| 217 |
-
` : '—'}
|
| 218 |
-
</span>
|
| 219 |
-
</div>
|
| 220 |
-
<div class="stat-metric">
|
| 221 |
-
<span class="metric-label">Updated</span>
|
| 222 |
-
<span class="metric-value">
|
| 223 |
-
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 4px; opacity: 0.6;">
|
| 224 |
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 225 |
-
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 226 |
-
</svg>
|
| 227 |
-
${new Date().toLocaleTimeString()}
|
| 228 |
-
</span>
|
| 229 |
-
</div>
|
| 230 |
-
</div>
|
| 231 |
</div>
|
| 232 |
-
|
| 233 |
-
}
|
| 234 |
)
|
| 235 |
.join('');
|
| 236 |
}
|
| 237 |
|
| 238 |
async loadTopCoins() {
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
const response = await fetch('https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=10&page=1&sparkline=true');
|
| 242 |
-
const coins = await response.json();
|
| 243 |
-
|
| 244 |
-
const rows = coins.map((coin, index) => {
|
| 245 |
-
const sparklineId = `sparkline-${coin.id}`;
|
| 246 |
-
const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
|
| 247 |
-
|
| 248 |
-
return `
|
| 249 |
-
<tr>
|
| 250 |
-
<td>${index + 1}</td>
|
| 251 |
-
<td>
|
| 252 |
-
<div class="chip">${coin.symbol.toUpperCase()}</div>
|
| 253 |
-
</td>
|
| 254 |
-
<td>
|
| 255 |
-
<div style="display: flex; align-items: center; gap: 8px;">
|
| 256 |
-
<img src="${coin.image}" alt="${coin.name}" style="width: 24px; height: 24px; border-radius: 50%;">
|
| 257 |
-
<span>${coin.name}</span>
|
| 258 |
-
</div>
|
| 259 |
-
</td>
|
| 260 |
-
<td style="font-weight: 600;">${formatCurrency(coin.current_price)}</td>
|
| 261 |
-
<td class="${coin.price_change_percentage_24h >= 0 ? 'text-success' : 'text-danger'}">
|
| 262 |
-
<span class="table-change-icon ${coin.price_change_percentage_24h >= 0 ? 'positive' : 'negative'}">
|
| 263 |
-
${coin.price_change_percentage_24h >= 0 ?
|
| 264 |
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' :
|
| 265 |
-
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
| 266 |
-
}
|
| 267 |
-
</span>
|
| 268 |
-
${formatPercent(coin.price_change_percentage_24h)}
|
| 269 |
-
</td>
|
| 270 |
-
<td>${formatCurrency(coin.total_volume)}</td>
|
| 271 |
-
<td>${formatCurrency(coin.market_cap)}</td>
|
| 272 |
-
<td>
|
| 273 |
-
<div style="width: 100px; height: 40px;">
|
| 274 |
-
<canvas id="${sparklineId}" width="100" height="40"></canvas>
|
| 275 |
-
</div>
|
| 276 |
-
</td>
|
| 277 |
-
</tr>
|
| 278 |
-
`;
|
| 279 |
-
});
|
| 280 |
-
|
| 281 |
-
this.topCoinsBody.innerHTML = rows.join('');
|
| 282 |
-
|
| 283 |
-
// Create sparkline charts after DOM update
|
| 284 |
-
setTimeout(() => {
|
| 285 |
-
coins.forEach(coin => {
|
| 286 |
-
if (coin.sparkline_in_7d && coin.sparkline_in_7d.price) {
|
| 287 |
-
const sparklineId = `sparkline-${coin.id}`;
|
| 288 |
-
const changeColor = coin.price_change_percentage_24h >= 0 ? '#4ade80' : '#ef4444';
|
| 289 |
-
createSparkline(sparklineId, coin.sparkline_in_7d.price.slice(-24), changeColor);
|
| 290 |
-
}
|
| 291 |
-
});
|
| 292 |
-
}, 100);
|
| 293 |
-
|
| 294 |
-
} catch (error) {
|
| 295 |
-
console.error('Error loading top coins:', error);
|
| 296 |
this.topCoinsBody.innerHTML = `
|
| 297 |
-
<tr><td colspan="
|
| 298 |
<div class="inline-message inline-error">
|
| 299 |
<strong>Failed to load coins</strong>
|
| 300 |
-
<p>${error
|
| 301 |
</div>
|
| 302 |
</td></tr>`;
|
|
|
|
| 303 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
}
|
| 305 |
|
| 306 |
async loadSentiment() {
|
| 307 |
if (!this.sentimentCanvas) return;
|
| 308 |
-
const container = this.sentimentCanvas.closest('.glass-card');
|
| 309 |
-
if (!container) return;
|
| 310 |
-
|
| 311 |
const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' });
|
| 312 |
if (!result.ok) {
|
| 313 |
-
|
| 314 |
return;
|
| 315 |
}
|
| 316 |
const payload = result.data || {};
|
|
@@ -320,142 +97,40 @@ class OverviewView {
|
|
| 320 |
neutral: sentiment.neutral ?? 35,
|
| 321 |
bearish: sentiment.bearish ?? 25,
|
| 322 |
};
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
</div>
|
| 348 |
-
<div class="sentiment-progress">
|
| 349 |
-
<div class="sentiment-progress-bar" style="width: ${bullishPct}%; background: linear-gradient(90deg, #22c55e, #16a34a);"></div>
|
| 350 |
-
</div>
|
| 351 |
-
</div>
|
| 352 |
-
<div class="sentiment-item neutral">
|
| 353 |
-
<div class="sentiment-item-header">
|
| 354 |
-
<div class="sentiment-icon">
|
| 355 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 356 |
-
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 357 |
-
<path d="M8 12h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 358 |
-
</svg>
|
| 359 |
-
</div>
|
| 360 |
-
<span class="sentiment-label">Neutral</span>
|
| 361 |
-
<span class="sentiment-percent">${neutralPct}%</span>
|
| 362 |
-
</div>
|
| 363 |
-
<div class="sentiment-progress">
|
| 364 |
-
<div class="sentiment-progress-bar" style="width: ${neutralPct}%; background: linear-gradient(90deg, #38bdf8, #0ea5e9);"></div>
|
| 365 |
-
</div>
|
| 366 |
-
</div>
|
| 367 |
-
<div class="sentiment-item bearish">
|
| 368 |
-
<div class="sentiment-item-header">
|
| 369 |
-
<div class="sentiment-icon">
|
| 370 |
-
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 371 |
-
<path d="M12 22L8.91 15.74L2 14.73L7 9.86L5.82 2.98L12 6.23L18.18 2.98L17 9.86L22 14.73L15.09 15.74L12 22Z" fill="currentColor"/>
|
| 372 |
-
</svg>
|
| 373 |
-
</div>
|
| 374 |
-
<span class="sentiment-label">Bearish</span>
|
| 375 |
-
<span class="sentiment-percent">${bearishPct}%</span>
|
| 376 |
-
</div>
|
| 377 |
-
<div class="sentiment-progress">
|
| 378 |
-
<div class="sentiment-progress-bar" style="width: ${bearishPct}%; background: linear-gradient(90deg, #ef4444, #dc2626);"></div>
|
| 379 |
-
</div>
|
| 380 |
-
</div>
|
| 381 |
-
</div>
|
| 382 |
-
<div class="sentiment-summary">
|
| 383 |
-
<div class="sentiment-summary-item">
|
| 384 |
-
<span class="summary-label">Overall</span>
|
| 385 |
-
<span class="summary-value ${data.bullish > data.bearish ? 'bullish' : data.bearish > data.bullish ? 'bearish' : 'neutral'}">
|
| 386 |
-
${data.bullish > data.bearish ? 'Bullish' : data.bearish > data.bullish ? 'Bearish' : 'Neutral'}
|
| 387 |
-
</span>
|
| 388 |
-
</div>
|
| 389 |
-
<div class="sentiment-summary-item">
|
| 390 |
-
<span class="summary-label">Confidence</span>
|
| 391 |
-
<span class="summary-value">${Math.max(bullishPct, neutralPct, bearishPct)}%</span>
|
| 392 |
-
</div>
|
| 393 |
-
</div>
|
| 394 |
-
</div>
|
| 395 |
-
`;
|
| 396 |
}
|
| 397 |
|
| 398 |
buildSentimentFallback(message) {
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
</div>
|
| 405 |
-
<div class="inline-message inline-info" style="margin-top: 1rem;">
|
| 406 |
-
<strong>Sentiment insight unavailable</strong>
|
| 407 |
-
<p>${message || 'AI sentiment endpoint did not respond in time.'}</p>
|
| 408 |
-
</div>
|
| 409 |
-
</div>
|
| 410 |
`;
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
async loadBackendInfo() {
|
| 414 |
-
const backendInfoContainer = this.section.querySelector('[data-backend-info]');
|
| 415 |
-
if (!backendInfoContainer) return;
|
| 416 |
-
|
| 417 |
-
try {
|
| 418 |
-
// Get API health
|
| 419 |
-
const healthResult = await apiClient.getHealth();
|
| 420 |
-
const apiStatusEl = this.section.querySelector('[data-api-status]');
|
| 421 |
-
if (apiStatusEl) {
|
| 422 |
-
if (healthResult.ok) {
|
| 423 |
-
apiStatusEl.textContent = 'Healthy';
|
| 424 |
-
apiStatusEl.style.color = '#22c55e';
|
| 425 |
-
} else {
|
| 426 |
-
apiStatusEl.textContent = 'Error';
|
| 427 |
-
apiStatusEl.style.color = '#ef4444';
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
// Get providers count
|
| 432 |
-
const providersResult = await apiClient.getProviders();
|
| 433 |
-
const providersCountEl = this.section.querySelector('[data-providers-count]');
|
| 434 |
-
if (providersCountEl && providersResult.ok) {
|
| 435 |
-
const providers = providersResult.data?.providers || providersResult.data || [];
|
| 436 |
-
const activeCount = Array.isArray(providers) ? providers.filter(p => p.status === 'active' || p.status === 'online').length : 0;
|
| 437 |
-
const totalCount = Array.isArray(providers) ? providers.length : 0;
|
| 438 |
-
providersCountEl.textContent = `${activeCount}/${totalCount} Active`;
|
| 439 |
-
providersCountEl.style.color = activeCount > 0 ? '#22c55e' : '#ef4444';
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
// Update last update time
|
| 443 |
-
const lastUpdateEl = this.section.querySelector('[data-last-update]');
|
| 444 |
-
if (lastUpdateEl) {
|
| 445 |
-
lastUpdateEl.textContent = new Date().toLocaleTimeString();
|
| 446 |
-
lastUpdateEl.style.color = 'var(--text-secondary)';
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
// WebSocket status is handled by app.js
|
| 450 |
-
const wsStatusEl = this.section.querySelector('[data-ws-status]');
|
| 451 |
-
if (wsStatusEl) {
|
| 452 |
-
// Will be updated by wsClient status change handler
|
| 453 |
-
wsStatusEl.textContent = 'Checking...';
|
| 454 |
-
wsStatusEl.style.color = '#f59e0b';
|
| 455 |
-
}
|
| 456 |
-
} catch (error) {
|
| 457 |
-
console.error('Error loading backend info:', error);
|
| 458 |
-
}
|
| 459 |
}
|
| 460 |
}
|
| 461 |
|
|
|
|
| 1 |
import apiClient from './apiClient.js';
|
| 2 |
import { formatCurrency, formatPercent, renderMessage, createSkeletonRows } from './uiUtils.js';
|
|
|
|
| 3 |
|
| 4 |
class OverviewView {
|
| 5 |
constructor(section) {
|
|
|
|
| 7 |
this.statsContainer = section.querySelector('[data-overview-stats]');
|
| 8 |
this.topCoinsBody = section.querySelector('[data-top-coins-body]');
|
| 9 |
this.sentimentCanvas = section.querySelector('#sentiment-chart');
|
|
|
|
| 10 |
this.sentimentChart = null;
|
|
|
|
| 11 |
}
|
| 12 |
|
| 13 |
async init() {
|
| 14 |
this.renderStatSkeletons();
|
| 15 |
+
this.topCoinsBody.innerHTML = createSkeletonRows(6, 6);
|
| 16 |
+
await Promise.all([this.loadStats(), this.loadTopCoins(), this.loadSentiment()]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
renderStatSkeletons() {
|
|
|
|
| 34 |
});
|
| 35 |
return;
|
| 36 |
}
|
| 37 |
+
const stats = result.data || {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
const cards = [
|
| 39 |
+
{ label: 'Total Market Cap', value: formatCurrency(stats.total_market_cap) },
|
| 40 |
+
{ label: '24h Volume', value: formatCurrency(stats.total_volume_24h) },
|
| 41 |
+
{ label: 'BTC Dominance', value: formatPercent(stats.btc_dominance) },
|
| 42 |
+
{ label: 'ETH Dominance', value: formatPercent(stats.eth_dominance) },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
];
|
| 44 |
this.statsContainer.innerHTML = cards
|
| 45 |
.map(
|
| 46 |
+
(card) => `
|
| 47 |
+
<div class="glass-card stat-card">
|
| 48 |
+
<h3>${card.label}</h3>
|
| 49 |
+
<div class="stat-value">${card.value}</div>
|
| 50 |
+
<div class="stat-trend">Updated ${new Date().toLocaleTimeString()}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
</div>
|
| 52 |
+
`,
|
|
|
|
| 53 |
)
|
| 54 |
.join('');
|
| 55 |
}
|
| 56 |
|
| 57 |
async loadTopCoins() {
|
| 58 |
+
const result = await apiClient.getTopCoins(10);
|
| 59 |
+
if (!result.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
this.topCoinsBody.innerHTML = `
|
| 61 |
+
<tr><td colspan="7">
|
| 62 |
<div class="inline-message inline-error">
|
| 63 |
<strong>Failed to load coins</strong>
|
| 64 |
+
<p>${result.error}</p>
|
| 65 |
</div>
|
| 66 |
</td></tr>`;
|
| 67 |
+
return;
|
| 68 |
}
|
| 69 |
+
const rows = (result.data || []).map(
|
| 70 |
+
(coin, index) => `
|
| 71 |
+
<tr>
|
| 72 |
+
<td>${index + 1}</td>
|
| 73 |
+
<td>${coin.symbol || coin.ticker || '—'}</td>
|
| 74 |
+
<td>${coin.name || 'Unknown'}</td>
|
| 75 |
+
<td>${formatCurrency(coin.price)}</td>
|
| 76 |
+
<td class="${coin.change_24h >= 0 ? 'text-success' : 'text-danger'}">
|
| 77 |
+
${formatPercent(coin.change_24h)}
|
| 78 |
+
</td>
|
| 79 |
+
<td>${formatCurrency(coin.volume_24h)}</td>
|
| 80 |
+
<td>${formatCurrency(coin.market_cap)}</td>
|
| 81 |
+
</tr>
|
| 82 |
+
`);
|
| 83 |
+
this.topCoinsBody.innerHTML = rows.join('');
|
| 84 |
}
|
| 85 |
|
| 86 |
async loadSentiment() {
|
| 87 |
if (!this.sentimentCanvas) return;
|
|
|
|
|
|
|
|
|
|
| 88 |
const result = await apiClient.runQuery({ query: 'global crypto sentiment breakdown' });
|
| 89 |
if (!result.ok) {
|
| 90 |
+
this.sentimentCanvas.replaceWith(this.buildSentimentFallback(result.error));
|
| 91 |
return;
|
| 92 |
}
|
| 93 |
const payload = result.data || {};
|
|
|
|
| 97 |
neutral: sentiment.neutral ?? 35,
|
| 98 |
bearish: sentiment.bearish ?? 25,
|
| 99 |
};
|
| 100 |
+
if (this.sentimentChart) {
|
| 101 |
+
this.sentimentChart.destroy();
|
| 102 |
+
}
|
| 103 |
+
this.sentimentChart = new Chart(this.sentimentCanvas, {
|
| 104 |
+
type: 'doughnut',
|
| 105 |
+
data: {
|
| 106 |
+
labels: ['Bullish', 'Neutral', 'Bearish'],
|
| 107 |
+
datasets: [
|
| 108 |
+
{
|
| 109 |
+
data: [data.bullish, data.neutral, data.bearish],
|
| 110 |
+
backgroundColor: ['#22c55e', '#38bdf8', '#ef4444'],
|
| 111 |
+
borderWidth: 0,
|
| 112 |
+
},
|
| 113 |
+
],
|
| 114 |
+
},
|
| 115 |
+
options: {
|
| 116 |
+
cutout: '65%',
|
| 117 |
+
plugins: {
|
| 118 |
+
legend: {
|
| 119 |
+
labels: { color: 'var(--text-primary)', usePointStyle: true },
|
| 120 |
+
},
|
| 121 |
+
},
|
| 122 |
+
},
|
| 123 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
buildSentimentFallback(message) {
|
| 127 |
+
const wrapper = document.createElement('div');
|
| 128 |
+
wrapper.className = 'inline-message inline-info';
|
| 129 |
+
wrapper.innerHTML = `
|
| 130 |
+
<strong>Sentiment insight unavailable</strong>
|
| 131 |
+
<p>${message || 'AI sentiment endpoint did not respond in time.'}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
`;
|
| 133 |
+
return wrapper;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
static/js/providersView.js
CHANGED
|
@@ -33,7 +33,6 @@ class ProvidersView {
|
|
| 33 |
this.tableBody.innerHTML = `<tr><td colspan="5"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
|
| 34 |
return;
|
| 35 |
}
|
| 36 |
-
// Backend returns {providers: [...], total: ..., ...}, so access result.data.providers
|
| 37 |
const data = result.data || {};
|
| 38 |
this.providers = data.providers || data || [];
|
| 39 |
this.applyFilters();
|
|
|
|
| 33 |
this.tableBody.innerHTML = `<tr><td colspan="5"><div class="inline-message inline-error">${result.error}</div></td></tr>`;
|
| 34 |
return;
|
| 35 |
}
|
|
|
|
| 36 |
const data = result.data || {};
|
| 37 |
this.providers = data.providers || data || [];
|
| 38 |
this.applyFilters();
|
static/js/uiUtils.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
| 1 |
export function formatCurrency(value) {
|
| 2 |
-
if (value === null || value === undefined || value
|
| 3 |
return '—';
|
| 4 |
}
|
| 5 |
const num = Number(value);
|
| 6 |
-
if (Number.isNaN(num)) {
|
| 7 |
-
return '—';
|
| 8 |
-
}
|
| 9 |
-
// Don't return '—' for 0, show $0.00 instead
|
| 10 |
-
if (num === 0) {
|
| 11 |
-
return '$0.00';
|
| 12 |
-
}
|
| 13 |
if (Math.abs(num) >= 1_000_000_000_000) {
|
| 14 |
return `$${(num / 1_000_000_000_000).toFixed(2)}T`;
|
| 15 |
}
|
|
@@ -19,10 +12,7 @@ export function formatCurrency(value) {
|
|
| 19 |
if (Math.abs(num) >= 1_000_000) {
|
| 20 |
return `$${(num / 1_000_000).toFixed(2)}M`;
|
| 21 |
}
|
| 22 |
-
|
| 23 |
-
return `$${(num / 1_000).toFixed(2)}K`;
|
| 24 |
-
}
|
| 25 |
-
return `$${num.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })}`;
|
| 26 |
}
|
| 27 |
|
| 28 |
export function formatPercent(value) {
|
|
|
|
| 1 |
export function formatCurrency(value) {
|
| 2 |
+
if (value === null || value === undefined || Number.isNaN(Number(value))) {
|
| 3 |
return '—';
|
| 4 |
}
|
| 5 |
const num = Number(value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
if (Math.abs(num) >= 1_000_000_000_000) {
|
| 7 |
return `$${(num / 1_000_000_000_000).toFixed(2)}T`;
|
| 8 |
}
|
|
|
|
| 12 |
if (Math.abs(num) >= 1_000_000) {
|
| 13 |
return `$${(num / 1_000_000).toFixed(2)}M`;
|
| 14 |
}
|
| 15 |
+
return `$${num.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
|
| 18 |
export function formatPercent(value) {
|
static/js/wsClient.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* WebSocket Client for Real-time Communication
|
| 3 |
-
* Manages WebSocket connections with automatic reconnection and exponential backoff
|
| 4 |
-
* Supports message routing to type-specific subscribers
|
| 5 |
-
*/
|
| 6 |
class WSClient {
|
| 7 |
constructor() {
|
| 8 |
this.socket = null;
|
|
@@ -11,83 +6,35 @@ class WSClient {
|
|
| 11 |
this.globalSubscribers = new Set();
|
| 12 |
this.typeSubscribers = new Map();
|
| 13 |
this.eventLog = [];
|
| 14 |
-
this.backoff = 1000;
|
| 15 |
-
this.maxBackoff = 16000;
|
| 16 |
this.shouldReconnect = true;
|
| 17 |
-
this.reconnectAttempts = 0;
|
| 18 |
-
this.connectionStartTime = null;
|
| 19 |
}
|
| 20 |
|
| 21 |
-
/**
|
| 22 |
-
* Automatically determine WebSocket URL based on environment
|
| 23 |
-
* Uses localhost for local development, HuggingFace Space URL for production
|
| 24 |
-
*/
|
| 25 |
get url() {
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
} else {
|
| 30 |
-
return 'wss://really-amin-datasourceforcryptocurrency.hf.space/ws';
|
| 31 |
-
}
|
| 32 |
}
|
| 33 |
|
| 34 |
-
/**
|
| 35 |
-
* Log WebSocket events for debugging and monitoring
|
| 36 |
-
* Maintains a rolling buffer of the last 100 events
|
| 37 |
-
* @param {Object} event - Event object to log
|
| 38 |
-
*/
|
| 39 |
logEvent(event) {
|
| 40 |
-
const entry = {
|
| 41 |
-
...event,
|
| 42 |
-
time: new Date().toISOString(),
|
| 43 |
-
attempt: this.reconnectAttempts
|
| 44 |
-
};
|
| 45 |
this.eventLog.push(entry);
|
| 46 |
-
|
| 47 |
-
if (this.eventLog.length > 100) {
|
| 48 |
-
this.eventLog = this.eventLog.slice(-100);
|
| 49 |
-
}
|
| 50 |
-
console.log('[WSClient]', entry);
|
| 51 |
}
|
| 52 |
|
| 53 |
-
/**
|
| 54 |
-
* Subscribe to connection status changes
|
| 55 |
-
* @param {Function} callback - Called with new status ('connecting', 'connected', 'disconnected', 'error')
|
| 56 |
-
* @returns {Function} Unsubscribe function
|
| 57 |
-
*/
|
| 58 |
onStatusChange(callback) {
|
| 59 |
-
if (typeof callback !== 'function') {
|
| 60 |
-
throw new Error('Callback must be a function');
|
| 61 |
-
}
|
| 62 |
this.statusSubscribers.add(callback);
|
| 63 |
-
// Immediately call with current status
|
| 64 |
callback(this.status);
|
| 65 |
return () => this.statusSubscribers.delete(callback);
|
| 66 |
}
|
| 67 |
|
| 68 |
-
/**
|
| 69 |
-
* Subscribe to all WebSocket messages
|
| 70 |
-
* @param {Function} callback - Called with parsed message data
|
| 71 |
-
* @returns {Function} Unsubscribe function
|
| 72 |
-
*/
|
| 73 |
onMessage(callback) {
|
| 74 |
-
if (typeof callback !== 'function') {
|
| 75 |
-
throw new Error('Callback must be a function');
|
| 76 |
-
}
|
| 77 |
this.globalSubscribers.add(callback);
|
| 78 |
return () => this.globalSubscribers.delete(callback);
|
| 79 |
}
|
| 80 |
|
| 81 |
-
/**
|
| 82 |
-
* Subscribe to specific message types
|
| 83 |
-
* @param {string} type - Message type to subscribe to (e.g., 'market_update', 'news_update')
|
| 84 |
-
* @param {Function} callback - Called with messages of the specified type
|
| 85 |
-
* @returns {Function} Unsubscribe function
|
| 86 |
-
*/
|
| 87 |
subscribe(type, callback) {
|
| 88 |
-
if (typeof callback !== 'function') {
|
| 89 |
-
throw new Error('Callback must be a function');
|
| 90 |
-
}
|
| 91 |
if (!this.typeSubscribers.has(type)) {
|
| 92 |
this.typeSubscribers.set(type, new Set());
|
| 93 |
}
|
|
@@ -96,269 +43,69 @@ class WSClient {
|
|
| 96 |
return () => set.delete(callback);
|
| 97 |
}
|
| 98 |
|
| 99 |
-
/**
|
| 100 |
-
* Update connection status and notify all subscribers
|
| 101 |
-
* @param {string} newStatus - New status value
|
| 102 |
-
*/
|
| 103 |
updateStatus(newStatus) {
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
this.status = newStatus;
|
| 107 |
-
this.logEvent({
|
| 108 |
-
type: 'status_change',
|
| 109 |
-
from: oldStatus,
|
| 110 |
-
to: newStatus
|
| 111 |
-
});
|
| 112 |
-
this.statusSubscribers.forEach(cb => {
|
| 113 |
-
try {
|
| 114 |
-
cb(newStatus);
|
| 115 |
-
} catch (error) {
|
| 116 |
-
console.error('[WSClient] Error in status subscriber:', error);
|
| 117 |
-
}
|
| 118 |
-
});
|
| 119 |
-
}
|
| 120 |
}
|
| 121 |
|
| 122 |
-
/**
|
| 123 |
-
* Establish WebSocket connection with automatic reconnection
|
| 124 |
-
* Implements exponential backoff for reconnection attempts
|
| 125 |
-
*/
|
| 126 |
connect() {
|
| 127 |
-
|
| 128 |
-
if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) {
|
| 129 |
-
console.log('[WSClient] Already connected or connecting');
|
| 130 |
return;
|
| 131 |
}
|
| 132 |
|
| 133 |
-
this.connectionStartTime = Date.now();
|
| 134 |
this.updateStatus('connecting');
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
this.
|
| 148 |
-
this.
|
| 149 |
-
this.
|
| 150 |
-
type
|
| 151 |
-
connectionTime: `${connectionTime}ms`
|
| 152 |
-
});
|
| 153 |
-
console.log(`[WSClient] Connected to ${this.url} in ${connectionTime}ms`);
|
| 154 |
-
};
|
| 155 |
-
|
| 156 |
-
this.socket.onmessage = (event) => {
|
| 157 |
-
try {
|
| 158 |
-
const data = JSON.parse(event.data);
|
| 159 |
-
this.logEvent({
|
| 160 |
-
type: 'message_received',
|
| 161 |
-
messageType: data.type || 'unknown',
|
| 162 |
-
size: event.data.length
|
| 163 |
-
});
|
| 164 |
-
|
| 165 |
-
// Notify global subscribers
|
| 166 |
-
this.globalSubscribers.forEach(cb => {
|
| 167 |
-
try {
|
| 168 |
-
cb(data);
|
| 169 |
-
} catch (error) {
|
| 170 |
-
console.error('[WSClient] Error in global subscriber:', error);
|
| 171 |
-
}
|
| 172 |
-
});
|
| 173 |
-
|
| 174 |
-
// Notify type-specific subscribers
|
| 175 |
-
if (data.type && this.typeSubscribers.has(data.type)) {
|
| 176 |
-
this.typeSubscribers.get(data.type).forEach(cb => {
|
| 177 |
-
try {
|
| 178 |
-
cb(data);
|
| 179 |
-
} catch (error) {
|
| 180 |
-
console.error(`[WSClient] Error in ${data.type} subscriber:`, error);
|
| 181 |
-
}
|
| 182 |
-
});
|
| 183 |
-
}
|
| 184 |
-
} catch (error) {
|
| 185 |
-
console.error('[WSClient] Message parse error:', error);
|
| 186 |
-
this.logEvent({
|
| 187 |
-
type: 'parse_error',
|
| 188 |
-
error: error.message,
|
| 189 |
-
rawData: event.data.substring(0, 100)
|
| 190 |
-
});
|
| 191 |
-
}
|
| 192 |
-
};
|
| 193 |
-
|
| 194 |
-
this.socket.onclose = (event) => {
|
| 195 |
-
const wasConnected = this.status === 'connected';
|
| 196 |
-
this.updateStatus('disconnected');
|
| 197 |
-
this.logEvent({
|
| 198 |
-
type: 'connection_closed',
|
| 199 |
-
code: event.code,
|
| 200 |
-
reason: event.reason || 'No reason provided',
|
| 201 |
-
wasClean: event.wasClean
|
| 202 |
-
});
|
| 203 |
-
|
| 204 |
-
// Attempt reconnection if enabled
|
| 205 |
-
if (this.shouldReconnect) {
|
| 206 |
-
this.reconnectAttempts++;
|
| 207 |
-
const delay = this.backoff;
|
| 208 |
-
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
| 209 |
-
|
| 210 |
-
console.log(`[WSClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})...`);
|
| 211 |
-
this.logEvent({
|
| 212 |
-
type: 'reconnect_scheduled',
|
| 213 |
-
delay: `${delay}ms`,
|
| 214 |
-
nextBackoff: `${this.backoff}ms`
|
| 215 |
-
});
|
| 216 |
-
|
| 217 |
-
setTimeout(() => this.connect(), delay);
|
| 218 |
}
|
| 219 |
-
}
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
this.logEvent({
|
| 225 |
-
type: 'connection_error',
|
| 226 |
-
error: error.message || 'Unknown error',
|
| 227 |
-
readyState: this.socket ? this.socket.readyState : 'null'
|
| 228 |
-
});
|
| 229 |
-
};
|
| 230 |
-
} catch (error) {
|
| 231 |
-
console.error('[WSClient] Failed to create WebSocket:', error);
|
| 232 |
-
this.updateStatus('error');
|
| 233 |
-
this.logEvent({
|
| 234 |
-
type: 'creation_error',
|
| 235 |
-
error: error.message
|
| 236 |
-
});
|
| 237 |
-
|
| 238 |
-
// Retry connection if enabled
|
| 239 |
if (this.shouldReconnect) {
|
| 240 |
-
this.reconnectAttempts++;
|
| 241 |
const delay = this.backoff;
|
| 242 |
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
| 243 |
setTimeout(() => this.connect(), delay);
|
| 244 |
}
|
| 245 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
}
|
| 247 |
|
| 248 |
-
/**
|
| 249 |
-
* Gracefully disconnect WebSocket and disable automatic reconnection
|
| 250 |
-
*/
|
| 251 |
disconnect() {
|
| 252 |
this.shouldReconnect = false;
|
| 253 |
if (this.socket) {
|
| 254 |
-
this.
|
| 255 |
-
this.socket.close(1000, 'Client disconnect');
|
| 256 |
-
this.socket = null;
|
| 257 |
}
|
| 258 |
}
|
| 259 |
|
| 260 |
-
/**
|
| 261 |
-
* Manually trigger reconnection (useful for testing or recovery)
|
| 262 |
-
*/
|
| 263 |
-
reconnect() {
|
| 264 |
-
this.disconnect();
|
| 265 |
-
this.shouldReconnect = true;
|
| 266 |
-
this.backoff = 1000; // Reset backoff
|
| 267 |
-
this.reconnectAttempts = 0;
|
| 268 |
-
this.connect();
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
/**
|
| 272 |
-
* Send a message through the WebSocket connection
|
| 273 |
-
* @param {Object} data - Data to send (will be JSON stringified)
|
| 274 |
-
* @returns {boolean} True if sent successfully, false otherwise
|
| 275 |
-
*/
|
| 276 |
-
send(data) {
|
| 277 |
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
| 278 |
-
console.error('[WSClient] Cannot send message: not connected');
|
| 279 |
-
this.logEvent({
|
| 280 |
-
type: 'send_failed',
|
| 281 |
-
reason: 'not_connected',
|
| 282 |
-
readyState: this.socket ? this.socket.readyState : 'null'
|
| 283 |
-
});
|
| 284 |
-
return false;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
try {
|
| 288 |
-
const message = JSON.stringify(data);
|
| 289 |
-
this.socket.send(message);
|
| 290 |
-
this.logEvent({
|
| 291 |
-
type: 'message_sent',
|
| 292 |
-
messageType: data.type || 'unknown',
|
| 293 |
-
size: message.length
|
| 294 |
-
});
|
| 295 |
-
return true;
|
| 296 |
-
} catch (error) {
|
| 297 |
-
console.error('[WSClient] Failed to send message:', error);
|
| 298 |
-
this.logEvent({
|
| 299 |
-
type: 'send_error',
|
| 300 |
-
error: error.message
|
| 301 |
-
});
|
| 302 |
-
return false;
|
| 303 |
-
}
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
/**
|
| 307 |
-
* Get a copy of the event log
|
| 308 |
-
* @returns {Array} Array of logged events
|
| 309 |
-
*/
|
| 310 |
getEvents() {
|
| 311 |
return [...this.eventLog];
|
| 312 |
}
|
| 313 |
-
|
| 314 |
-
/**
|
| 315 |
-
* Get current connection statistics
|
| 316 |
-
* @returns {Object} Connection statistics
|
| 317 |
-
*/
|
| 318 |
-
getStats() {
|
| 319 |
-
return {
|
| 320 |
-
status: this.status,
|
| 321 |
-
reconnectAttempts: this.reconnectAttempts,
|
| 322 |
-
currentBackoff: this.backoff,
|
| 323 |
-
maxBackoff: this.maxBackoff,
|
| 324 |
-
shouldReconnect: this.shouldReconnect,
|
| 325 |
-
subscriberCounts: {
|
| 326 |
-
status: this.statusSubscribers.size,
|
| 327 |
-
global: this.globalSubscribers.size,
|
| 328 |
-
typed: Array.from(this.typeSubscribers.entries()).map(([type, subs]) => ({
|
| 329 |
-
type,
|
| 330 |
-
count: subs.size
|
| 331 |
-
}))
|
| 332 |
-
},
|
| 333 |
-
eventLogSize: this.eventLog.length,
|
| 334 |
-
url: this.url
|
| 335 |
-
};
|
| 336 |
-
}
|
| 337 |
-
|
| 338 |
-
/**
|
| 339 |
-
* Check if WebSocket is currently connected
|
| 340 |
-
* @returns {boolean} True if connected
|
| 341 |
-
*/
|
| 342 |
-
isConnected() {
|
| 343 |
-
return this.socket && this.socket.readyState === WebSocket.OPEN;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
/**
|
| 347 |
-
* Clear all subscribers (useful for cleanup)
|
| 348 |
-
*/
|
| 349 |
-
clearSubscribers() {
|
| 350 |
-
this.statusSubscribers.clear();
|
| 351 |
-
this.globalSubscribers.clear();
|
| 352 |
-
this.typeSubscribers.clear();
|
| 353 |
-
this.logEvent({ type: 'subscribers_cleared' });
|
| 354 |
-
}
|
| 355 |
}
|
| 356 |
|
| 357 |
-
// Create singleton instance
|
| 358 |
const wsClient = new WSClient();
|
| 359 |
-
|
| 360 |
-
// Auto-connect on module load
|
| 361 |
-
wsClient.connect();
|
| 362 |
-
|
| 363 |
-
// Export singleton instance
|
| 364 |
-
export default wsClient;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
class WSClient {
|
| 2 |
constructor() {
|
| 3 |
this.socket = null;
|
|
|
|
| 6 |
this.globalSubscribers = new Set();
|
| 7 |
this.typeSubscribers = new Map();
|
| 8 |
this.eventLog = [];
|
| 9 |
+
this.backoff = 1000;
|
| 10 |
+
this.maxBackoff = 16000;
|
| 11 |
this.shouldReconnect = true;
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
get url() {
|
| 15 |
+
const { protocol, host } = window.location;
|
| 16 |
+
const wsProtocol = protocol === 'https:' ? 'wss:' : 'ws:';
|
| 17 |
+
return `${wsProtocol}//${host}/ws`;
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
logEvent(event) {
|
| 21 |
+
const entry = { ...event, time: new Date().toISOString() };
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
this.eventLog.push(entry);
|
| 23 |
+
this.eventLog = this.eventLog.slice(-100);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
onStatusChange(callback) {
|
|
|
|
|
|
|
|
|
|
| 27 |
this.statusSubscribers.add(callback);
|
|
|
|
| 28 |
callback(this.status);
|
| 29 |
return () => this.statusSubscribers.delete(callback);
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
onMessage(callback) {
|
|
|
|
|
|
|
|
|
|
| 33 |
this.globalSubscribers.add(callback);
|
| 34 |
return () => this.globalSubscribers.delete(callback);
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
subscribe(type, callback) {
|
|
|
|
|
|
|
|
|
|
| 38 |
if (!this.typeSubscribers.has(type)) {
|
| 39 |
this.typeSubscribers.set(type, new Set());
|
| 40 |
}
|
|
|
|
| 43 |
return () => set.delete(callback);
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
updateStatus(newStatus) {
|
| 47 |
+
this.status = newStatus;
|
| 48 |
+
this.statusSubscribers.forEach((cb) => cb(newStatus));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
}
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
connect() {
|
| 52 |
+
if (this.socket && (this.status === 'connecting' || this.status === 'connected')) {
|
|
|
|
|
|
|
| 53 |
return;
|
| 54 |
}
|
| 55 |
|
|
|
|
| 56 |
this.updateStatus('connecting');
|
| 57 |
+
this.socket = new WebSocket(this.url);
|
| 58 |
+
this.logEvent({ type: 'status', status: 'connecting' });
|
| 59 |
+
|
| 60 |
+
this.socket.addEventListener('open', () => {
|
| 61 |
+
this.backoff = 1000;
|
| 62 |
+
this.updateStatus('connected');
|
| 63 |
+
this.logEvent({ type: 'status', status: 'connected' });
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
this.socket.addEventListener('message', (event) => {
|
| 67 |
+
try {
|
| 68 |
+
const data = JSON.parse(event.data);
|
| 69 |
+
this.logEvent({ type: 'message', messageType: data.type || 'unknown' });
|
| 70 |
+
this.globalSubscribers.forEach((cb) => cb(data));
|
| 71 |
+
if (data.type && this.typeSubscribers.has(data.type)) {
|
| 72 |
+
this.typeSubscribers.get(data.type).forEach((cb) => cb(data));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
+
} catch (error) {
|
| 75 |
+
console.error('WS message parse error', error);
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
|
| 79 |
+
this.socket.addEventListener('close', () => {
|
| 80 |
+
this.updateStatus('disconnected');
|
| 81 |
+
this.logEvent({ type: 'status', status: 'disconnected' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
if (this.shouldReconnect) {
|
|
|
|
| 83 |
const delay = this.backoff;
|
| 84 |
this.backoff = Math.min(this.backoff * 2, this.maxBackoff);
|
| 85 |
setTimeout(() => this.connect(), delay);
|
| 86 |
}
|
| 87 |
+
});
|
| 88 |
+
|
| 89 |
+
this.socket.addEventListener('error', (error) => {
|
| 90 |
+
console.error('WebSocket error', error);
|
| 91 |
+
this.logEvent({ type: 'error', details: error.message || 'unknown' });
|
| 92 |
+
if (this.socket) {
|
| 93 |
+
this.socket.close();
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
}
|
| 97 |
|
|
|
|
|
|
|
|
|
|
| 98 |
disconnect() {
|
| 99 |
this.shouldReconnect = false;
|
| 100 |
if (this.socket) {
|
| 101 |
+
this.socket.close();
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
getEvents() {
|
| 106 |
return [...this.eventLog];
|
| 107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
|
|
|
| 110 |
const wsClient = new WSClient();
|
| 111 |
+
export default wsClient;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unified_dashboard.html
CHANGED
|
@@ -8,152 +8,54 @@
|
|
| 8 |
<link rel="stylesheet" href="static/css/design-system.css" />
|
| 9 |
<link rel="stylesheet" href="static/css/dashboard.css" />
|
| 10 |
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
| 11 |
-
<link rel="stylesheet" href="static/css/modern-dashboard.css" />
|
| 12 |
-
<link rel="stylesheet" href="static/css/glassmorphism.css" />
|
| 13 |
-
<link rel="stylesheet" href="static/css/light-minimal-theme.css" />
|
| 14 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
|
| 15 |
-
<script src="static/js/animations.js" defer></script>
|
| 16 |
-
<script src="static/js/menu-system.js" defer></script>
|
| 17 |
-
<script src="static/js/huggingface-integration.js" defer></script>
|
| 18 |
</head>
|
| 19 |
-
<body data-theme="
|
| 20 |
<div class="app-shell">
|
| 21 |
-
<aside class="sidebar
|
| 22 |
-
<div class="brand
|
| 23 |
-
<
|
| 24 |
-
|
|
|
|
| 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 |
-
|
| 30 |
-
|
| 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
|
| 41 |
-
<button class="nav-button
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
</button>
|
| 48 |
-
<button class="nav-button
|
| 49 |
-
|
| 50 |
-
|
| 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 |
-
<
|
| 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="
|
| 120 |
-
<div
|
| 121 |
-
<
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
</div>
|
| 125 |
-
<div class="status-
|
| 126 |
-
<
|
| 127 |
-
|
| 128 |
-
<span>checking</span>
|
| 129 |
-
</div>
|
| 130 |
-
<div class="status-pill" data-ws-status data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
|
| 131 |
-
<span class="status-dot"></span>
|
| 132 |
-
<span>connecting</span>
|
| 133 |
-
</div>
|
| 134 |
-
<button class="button-3d" data-menu-trigger="theme-menu" style="padding: 8px 16px; position: relative;">
|
| 135 |
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 136 |
-
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
| 137 |
-
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 138 |
-
</svg>
|
| 139 |
-
</button>
|
| 140 |
-
<div class="menu-dropdown" data-menu="theme-menu" style="display: none; top: 100%; right: 0; margin-top: 8px;">
|
| 141 |
-
<div class="menu-item" data-action="theme-light">
|
| 142 |
-
<span>☀️ Light Theme</span>
|
| 143 |
-
</div>
|
| 144 |
-
<div class="menu-item" data-action="theme-dark">
|
| 145 |
-
<span>🌙 Dark Theme</span>
|
| 146 |
-
</div>
|
| 147 |
-
<div class="menu-separator"></div>
|
| 148 |
-
<div class="menu-item" data-action="settings">
|
| 149 |
-
<span>⚙️ Settings</span>
|
| 150 |
-
</div>
|
| 151 |
-
</div>
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
-
<div class="header-crypto-list" data-header-crypto-list style="margin-top: 16px; width: 100%;">
|
| 155 |
-
<!-- Crypto list will be populated by JavaScript -->
|
| 156 |
-
</div>
|
| 157 |
</header>
|
| 158 |
<div class="page-container">
|
| 159 |
<section id="page-overview" class="page active">
|
|
@@ -193,30 +95,6 @@
|
|
| 193 |
<canvas id="sentiment-chart" height="220"></canvas>
|
| 194 |
</div>
|
| 195 |
</div>
|
| 196 |
-
<div class="glass-card" style="margin-top: 24px;">
|
| 197 |
-
<div class="section-header">
|
| 198 |
-
<h3>Backend Information</h3>
|
| 199 |
-
<span class="text-muted">System Status</span>
|
| 200 |
-
</div>
|
| 201 |
-
<div data-backend-info class="backend-info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px;">
|
| 202 |
-
<div class="backend-info-item">
|
| 203 |
-
<span class="info-label">API Status</span>
|
| 204 |
-
<span class="info-value" data-api-status>Checking...</span>
|
| 205 |
-
</div>
|
| 206 |
-
<div class="backend-info-item">
|
| 207 |
-
<span class="info-label">WebSocket</span>
|
| 208 |
-
<span class="info-value" data-ws-status>Connecting...</span>
|
| 209 |
-
</div>
|
| 210 |
-
<div class="backend-info-item">
|
| 211 |
-
<span class="info-label">Providers</span>
|
| 212 |
-
<span class="info-value" data-providers-count>—</span>
|
| 213 |
-
</div>
|
| 214 |
-
<div class="backend-info-item">
|
| 215 |
-
<span class="info-label">Last Update</span>
|
| 216 |
-
<span class="info-value" data-last-update>—</span>
|
| 217 |
-
</div>
|
| 218 |
-
</div>
|
| 219 |
-
</div>
|
| 220 |
</section>
|
| 221 |
|
| 222 |
<section id="page-market" class="page">
|
|
@@ -275,53 +153,32 @@
|
|
| 275 |
|
| 276 |
<section id="page-chart" class="page">
|
| 277 |
<div class="section-header">
|
| 278 |
-
<h2 class="section-title">Chart Lab
|
| 279 |
<div class="controls-bar">
|
| 280 |
-
<select data-chart-symbol
|
| 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="
|
| 291 |
-
<button class="
|
| 292 |
-
<button class="
|
| 293 |
-
<button class="
|
| 294 |
-
<button class="chart-timeframe-btn" data-chart-timeframe="90d">90D</button>
|
| 295 |
</div>
|
| 296 |
</div>
|
| 297 |
</div>
|
| 298 |
-
<div class="
|
| 299 |
-
<
|
| 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
|
| 322 |
-
<div class="controls-bar"
|
| 323 |
-
<
|
|
|
|
|
|
|
|
|
|
| 324 |
</div>
|
|
|
|
| 325 |
<div data-ai-insights class="ai-insights"></div>
|
| 326 |
</div>
|
| 327 |
</section>
|
|
|
|
| 8 |
<link rel="stylesheet" href="static/css/design-system.css" />
|
| 9 |
<link rel="stylesheet" href="static/css/dashboard.css" />
|
| 10 |
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
|
|
|
|
|
|
|
|
|
| 11 |
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
|
|
|
|
|
|
|
|
|
|
| 12 |
</head>
|
| 13 |
+
<body data-theme="dark">
|
| 14 |
<div class="app-shell">
|
| 15 |
+
<aside class="sidebar">
|
| 16 |
+
<div class="brand">
|
| 17 |
+
<strong>Crypto Monitor HF</strong>
|
| 18 |
+
<span class="env-pill">
|
| 19 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 20 |
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
|
| 21 |
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
|
| 22 |
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
|
| 23 |
</svg>
|
| 24 |
+
HF Space
|
| 25 |
+
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
</div>
|
| 27 |
+
<nav class="nav">
|
| 28 |
+
<button class="nav-button active" data-nav="page-overview">Overview</button>
|
| 29 |
+
<button class="nav-button" data-nav="page-market">Market</button>
|
| 30 |
+
<button class="nav-button" data-nav="page-chart">Chart Lab</button>
|
| 31 |
+
<button class="nav-button" data-nav="page-ai">Sentiment & AI</button>
|
| 32 |
+
<button class="nav-button" data-nav="page-news">News</button>
|
| 33 |
+
<button class="nav-button" data-nav="page-providers">Providers</button>
|
| 34 |
+
<button class="nav-button" data-nav="page-api">API Explorer</button>
|
| 35 |
+
<button class="nav-button" data-nav="page-debug">Diagnostics</button>
|
| 36 |
+
<button class="nav-button" data-nav="page-datasets">Datasets & Models</button>
|
| 37 |
+
<button class="nav-button" data-nav="page-settings">Settings</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
</nav>
|
| 39 |
<div class="sidebar-footer">
|
| 40 |
+
Unified crypto intelligence console<br />Realtime data • HF optimized
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
</div>
|
| 42 |
</aside>
|
| 43 |
<main class="main-area">
|
| 44 |
+
<header class="topbar">
|
| 45 |
+
<div>
|
| 46 |
+
<h1>Unified Intelligence Dashboard</h1>
|
| 47 |
+
<p class="text-muted">Live market telemetry, AI signals, diagnostics, and provider health.</p>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="status-group">
|
| 50 |
+
<div class="status-pill" data-api-health data-state="warn">
|
| 51 |
+
<span class="status-dot"></span>
|
| 52 |
+
<span>checking</span>
|
| 53 |
</div>
|
| 54 |
+
<div class="status-pill" data-ws-status data-state="warn">
|
| 55 |
+
<span class="status-dot"></span>
|
| 56 |
+
<span>connecting</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
</div>
|
| 58 |
</div>
|
|
|
|
|
|
|
|
|
|
| 59 |
</header>
|
| 60 |
<div class="page-container">
|
| 61 |
<section id="page-overview" class="page active">
|
|
|
|
| 95 |
<canvas id="sentiment-chart" height="220"></canvas>
|
| 96 |
</div>
|
| 97 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</section>
|
| 99 |
|
| 100 |
<section id="page-market" class="page">
|
|
|
|
| 153 |
|
| 154 |
<section id="page-chart" class="page">
|
| 155 |
<div class="section-header">
|
| 156 |
+
<h2 class="section-title">Chart Lab</h2>
|
| 157 |
<div class="controls-bar">
|
| 158 |
+
<select data-chart-symbol>
|
| 159 |
<option value="BTC">BTC</option>
|
| 160 |
<option value="ETH">ETH</option>
|
| 161 |
<option value="SOL">SOL</option>
|
| 162 |
<option value="BNB">BNB</option>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
</select>
|
| 164 |
+
<div class="input-chip">
|
| 165 |
+
<button class="ghost active" data-chart-timeframe="7d">7D</button>
|
| 166 |
+
<button class="ghost" data-chart-timeframe="30d">30D</button>
|
| 167 |
+
<button class="ghost" data-chart-timeframe="90d">90D</button>
|
|
|
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
+
<div class="glass-card">
|
| 172 |
+
<canvas id="chart-lab-canvas" height="260"></canvas>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
</div>
|
| 174 |
+
<div class="glass-card">
|
| 175 |
+
<div class="controls-bar">
|
| 176 |
+
<label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
|
| 177 |
+
<label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
|
| 178 |
+
<label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
|
| 179 |
+
<label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
|
| 180 |
</div>
|
| 181 |
+
<button class="primary" data-run-analysis>Analyze Chart with AI</button>
|
| 182 |
<div data-ai-insights class="ai-insights"></div>
|
| 183 |
</div>
|
| 184 |
</section>
|