Upload 303 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +20 -0
- Dockerfile +37 -10
- TEST_ENDPOINTS.sh +161 -88
- admin.html +69 -1007
- ai_models.py +157 -38
- api-resources/crypto_resources_unified_2025-11-11.json +0 -0
- app.js +34 -1
- app.py +31 -0
- archive_html/admin_advanced.html +1862 -0
- archive_html/admin_improved.html +61 -0
- archive_html/admin_pro.html +657 -0
- archive_html/complete_dashboard.html +857 -0
- archive_html/crypto_dashboard_pro.html +441 -0
- archive_html/dashboard.html +113 -0
- archive_html/dashboard_standalone.html +410 -0
- archive_html/enhanced_dashboard.html +876 -0
- archive_html/feature_flags_demo.html +393 -0
- archive_html/hf_console.html +97 -0
- archive_html/improved_dashboard.html +443 -0
- archive_html/index (1).html +0 -0
- archive_html/index_backup.html +2452 -0
- archive_html/index_enhanced.html +2132 -0
- archive_html/pool_management.html +765 -0
- archive_html/simple_overview.html +303 -0
- archive_html/unified_dashboard.html +639 -0
- backend/routers/__pycache__/__init__.cpython-313.pyc +0 -0
- backend/routers/__pycache__/hf_connect.cpython-313.pyc +0 -0
- backend/services/__pycache__/hf_client.cpython-313.pyc +0 -0
- backend/services/__pycache__/hf_registry.cpython-313.pyc +0 -0
- backend/services/__pycache__/local_resource_service.cpython-313.pyc +0 -0
- backend/services/auto_discovery_service.py +4 -1
- backend/services/diagnostics_service.py +8 -1
- backend/services/hf_registry.py +68 -45
- backend/services/local_resource_service.py +207 -0
- check_server.py +101 -0
- collectors/__pycache__/aggregator.cpython-313.pyc +0 -0
- collectors/aggregator.py +113 -4
- config.js +372 -129
- config.py +10 -0
- crypto_resources_unified_2025-11-11.json +0 -0
- enhanced_server.py +3 -3
- hf_unified_server.py +1055 -113
- index.html +0 -0
- main.py +29 -17
- package-lock.json +908 -1
- package.json +13 -1
- production_server.py +1 -1
- provider_validator.py +7 -1
- pytest.ini +4 -0
- real_server.py +1 -1
.env
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HuggingFace Configuration
|
| 2 |
+
HUGGINGFACE_TOKEN=your_token_here
|
| 3 |
+
ENABLE_SENTIMENT=true
|
| 4 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 5 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 6 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
| 7 |
+
HF_HTTP_TIMEOUT=8.0
|
| 8 |
+
|
| 9 |
+
# Existing API Keys (if any)
|
| 10 |
+
ETHERSCAN_KEY_1=
|
| 11 |
+
ETHERSCAN_KEY_2=
|
| 12 |
+
BSCSCAN_KEY=
|
| 13 |
+
TRONSCAN_KEY=
|
| 14 |
+
COINMARKETCAP_KEY_1=
|
| 15 |
+
COINMARKETCAP_KEY_2=
|
| 16 |
+
NEWSAPI_KEY=
|
| 17 |
+
CRYPTOCOMPARE_KEY=
|
| 18 |
+
|
| 19 |
+
# HuggingFace API Token
|
| 20 |
+
HF_TOKEN=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
|
Dockerfile
CHANGED
|
@@ -1,24 +1,51 @@
|
|
| 1 |
-
FROM python:3.10
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
RUN
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
-
# Copy requirements
|
| 9 |
COPY requirements.txt .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
|
| 12 |
# Copy application code
|
| 13 |
COPY . .
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
# Expose port
|
| 21 |
EXPOSE 7860
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
git \
|
| 9 |
+
curl \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
+
# Copy requirements first for better caching
|
| 13 |
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Upgrade pip
|
| 16 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 17 |
+
|
| 18 |
+
# Install dependencies
|
| 19 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 20 |
|
| 21 |
# Copy application code
|
| 22 |
COPY . .
|
| 23 |
|
| 24 |
+
# Create necessary directories
|
| 25 |
+
RUN mkdir -p \
|
| 26 |
+
data/database \
|
| 27 |
+
data/backups \
|
| 28 |
+
logs \
|
| 29 |
+
static/css \
|
| 30 |
+
static/js \
|
| 31 |
+
.cache/huggingface
|
| 32 |
+
|
| 33 |
+
# Set permissions
|
| 34 |
+
RUN chmod -R 755 /app
|
| 35 |
+
|
| 36 |
+
# Environment variables
|
| 37 |
+
ENV PORT=7860 \
|
| 38 |
+
PYTHONUNBUFFERED=1 \
|
| 39 |
+
TRANSFORMERS_CACHE=/app/.cache/huggingface \
|
| 40 |
+
HF_HOME=/app/.cache/huggingface \
|
| 41 |
+
PYTHONDONTWRITEBYTECODE=1
|
| 42 |
|
| 43 |
# Expose port
|
| 44 |
EXPOSE 7860
|
| 45 |
|
| 46 |
+
# Health check
|
| 47 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
|
| 48 |
+
CMD curl -f http://localhost:7860/api/health || exit 1
|
| 49 |
+
|
| 50 |
+
# Run application
|
| 51 |
+
CMD ["uvicorn", "hf_unified_server:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
|
TEST_ENDPOINTS.sh
CHANGED
|
@@ -1,88 +1,161 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
#
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
local
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
if [ "$
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
echo "
|
| 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 |
-
echo "
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
test_endpoint "
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
echo ""
|
| 82 |
-
|
| 83 |
-
echo "
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
echo "
|
| 88 |
-
echo "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# API Endpoints Test Script
|
| 3 |
+
# Run this after starting the backend to verify all endpoints work
|
| 4 |
+
|
| 5 |
+
BASE_URL="${BASE_URL:-http://localhost:7860}"
|
| 6 |
+
GREEN='\033[0;32m'
|
| 7 |
+
RED='\033[0;31m'
|
| 8 |
+
YELLOW='\033[1;33m'
|
| 9 |
+
NC='\033[0m' # No Color
|
| 10 |
+
|
| 11 |
+
echo "======================================"
|
| 12 |
+
echo "🧪 Testing Crypto HF API Endpoints"
|
| 13 |
+
echo "======================================"
|
| 14 |
+
echo "Base URL: $BASE_URL"
|
| 15 |
+
echo ""
|
| 16 |
+
|
| 17 |
+
# Function to test endpoint
|
| 18 |
+
test_endpoint() {
|
| 19 |
+
local method=$1
|
| 20 |
+
local endpoint=$2
|
| 21 |
+
local data=$3
|
| 22 |
+
local name=$4
|
| 23 |
+
|
| 24 |
+
echo -n "Testing $name... "
|
| 25 |
+
|
| 26 |
+
if [ "$method" = "GET" ]; then
|
| 27 |
+
response=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$endpoint")
|
| 28 |
+
else
|
| 29 |
+
response=$(curl -s -o /dev/null -w "%{http_code}" -X "$method" "$BASE_URL$endpoint" \
|
| 30 |
+
-H "Content-Type: application/json" \
|
| 31 |
+
-d "$data")
|
| 32 |
+
fi
|
| 33 |
+
|
| 34 |
+
if [ "$response" = "200" ]; then
|
| 35 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $response)"
|
| 36 |
+
else
|
| 37 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $response)"
|
| 38 |
+
return 1
|
| 39 |
+
fi
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Test health
|
| 43 |
+
test_endpoint "GET" "/api/health" "" "Health Check"
|
| 44 |
+
|
| 45 |
+
# Test market endpoints
|
| 46 |
+
echo ""
|
| 47 |
+
echo "📊 Market Endpoints:"
|
| 48 |
+
test_endpoint "GET" "/api/coins/top?limit=5" "" "Top Coins"
|
| 49 |
+
test_endpoint "GET" "/api/coins/BTC" "" "Bitcoin Details"
|
| 50 |
+
test_endpoint "GET" "/api/market/stats" "" "Market Stats"
|
| 51 |
+
|
| 52 |
+
# Test chart endpoints
|
| 53 |
+
echo ""
|
| 54 |
+
echo "📈 Chart Endpoints:"
|
| 55 |
+
test_endpoint "GET" "/api/charts/price/BTC?timeframe=7d" "" "BTC Price Chart"
|
| 56 |
+
|
| 57 |
+
# POST endpoint for chart analyze
|
| 58 |
+
echo -n "Testing Chart Analysis... "
|
| 59 |
+
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/charts/analyze" \
|
| 60 |
+
-H "Content-Type: application/json" \
|
| 61 |
+
-d '{"symbol":"BTC","timeframe":"7d","indicators":[]}')
|
| 62 |
+
http_code=$(echo "$response" | tail -n1)
|
| 63 |
+
if [ "$http_code" = "200" ]; then
|
| 64 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 65 |
+
else
|
| 66 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
# Test news endpoints
|
| 70 |
+
echo ""
|
| 71 |
+
echo "📰 News Endpoints:"
|
| 72 |
+
test_endpoint "GET" "/api/news/latest?limit=5" "" "Latest News"
|
| 73 |
+
|
| 74 |
+
# POST endpoint for news summarize
|
| 75 |
+
echo -n "Testing News Summarize... "
|
| 76 |
+
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/news/summarize" \
|
| 77 |
+
-H "Content-Type: application/json" \
|
| 78 |
+
-d '{"title":"Bitcoin breaks new record","description":"BTC hits $50k"}')
|
| 79 |
+
http_code=$(echo "$response" | tail -n1)
|
| 80 |
+
if [ "$http_code" = "200" ]; then
|
| 81 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 82 |
+
else
|
| 83 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 84 |
+
fi
|
| 85 |
+
|
| 86 |
+
# Test AI endpoints
|
| 87 |
+
echo ""
|
| 88 |
+
echo "🤖 AI Endpoints:"
|
| 89 |
+
|
| 90 |
+
# POST endpoint for sentiment
|
| 91 |
+
echo -n "Testing Sentiment Analysis... "
|
| 92 |
+
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/sentiment/analyze" \
|
| 93 |
+
-H "Content-Type: application/json" \
|
| 94 |
+
-d '{"text":"Bitcoin is breaking new all-time highs!"}')
|
| 95 |
+
http_code=$(echo "$response" | tail -n1)
|
| 96 |
+
body=$(echo "$response" | head -n-1)
|
| 97 |
+
if [ "$http_code" = "200" ]; then
|
| 98 |
+
sentiment=$(echo "$body" | grep -o '"sentiment":"[^"]*"' | cut -d'"' -f4)
|
| 99 |
+
confidence=$(echo "$body" | grep -o '"confidence":[0-9.]*' | cut -d':' -f2)
|
| 100 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code) - Sentiment: ${YELLOW}$sentiment${NC} (${confidence})"
|
| 101 |
+
else
|
| 102 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 103 |
+
fi
|
| 104 |
+
|
| 105 |
+
# POST endpoint for query
|
| 106 |
+
echo -n "Testing Query... "
|
| 107 |
+
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/query" \
|
| 108 |
+
-H "Content-Type: application/json" \
|
| 109 |
+
-d '{"query":"What is the price of Bitcoin?"}')
|
| 110 |
+
http_code=$(echo "$response" | tail -n1)
|
| 111 |
+
if [ "$http_code" = "200" ]; then
|
| 112 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 113 |
+
else
|
| 114 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 115 |
+
fi
|
| 116 |
+
|
| 117 |
+
# Test provider endpoints
|
| 118 |
+
echo ""
|
| 119 |
+
echo "🔌 Provider Endpoints:"
|
| 120 |
+
test_endpoint "GET" "/api/providers" "" "Providers List"
|
| 121 |
+
|
| 122 |
+
# Test datasets endpoints
|
| 123 |
+
echo ""
|
| 124 |
+
echo "📚 Datasets & Models Endpoints:"
|
| 125 |
+
test_endpoint "GET" "/api/datasets/list" "" "Datasets List"
|
| 126 |
+
test_endpoint "GET" "/api/models/list" "" "Models List"
|
| 127 |
+
|
| 128 |
+
# POST endpoint for model test
|
| 129 |
+
echo -n "Testing Model Test... "
|
| 130 |
+
response=$(curl -s -w "\n%{http_code}" -X POST "$BASE_URL/api/models/test" \
|
| 131 |
+
-H "Content-Type: application/json" \
|
| 132 |
+
-d '{"model":"crypto_sent_0","text":"Ethereum price surging!"}')
|
| 133 |
+
http_code=$(echo "$response" | tail -n1)
|
| 134 |
+
if [ "$http_code" = "200" ]; then
|
| 135 |
+
echo -e "${GREEN}✅ OK${NC} (HTTP $http_code)"
|
| 136 |
+
else
|
| 137 |
+
echo -e "${RED}❌ FAILED${NC} (HTTP $http_code)"
|
| 138 |
+
fi
|
| 139 |
+
|
| 140 |
+
# Summary
|
| 141 |
+
echo ""
|
| 142 |
+
echo "======================================"
|
| 143 |
+
echo "📊 Test Summary"
|
| 144 |
+
echo "======================================"
|
| 145 |
+
echo ""
|
| 146 |
+
echo "✅ All critical endpoints tested"
|
| 147 |
+
echo ""
|
| 148 |
+
echo "🌐 Dashboard URLs:"
|
| 149 |
+
echo " - Main: $BASE_URL/"
|
| 150 |
+
echo " - Admin: $BASE_URL/admin.html"
|
| 151 |
+
echo " - API Docs: $BASE_URL/docs"
|
| 152 |
+
echo ""
|
| 153 |
+
echo "🔌 WebSocket:"
|
| 154 |
+
echo " - ws://$(echo $BASE_URL | sed 's|http://||')/ws"
|
| 155 |
+
echo ""
|
| 156 |
+
echo "💡 Next steps:"
|
| 157 |
+
echo " 1. Open $BASE_URL/ in your browser"
|
| 158 |
+
echo " 2. Check all dashboard tabs"
|
| 159 |
+
echo " 3. Verify WebSocket connection (status indicator)"
|
| 160 |
+
echo ""
|
| 161 |
+
echo "======================================"
|
admin.html
CHANGED
|
@@ -1,1017 +1,79 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="UTF-8"
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1
|
| 6 |
-
<title>
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 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 |
-
|
| 410 |
-
<
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 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 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
<
|
| 482 |
-
|
| 483 |
-
</
|
| 484 |
-
|
| 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 |
-
|
| 495 |
-
<
|
| 496 |
-
<div class="
|
| 497 |
-
<
|
| 498 |
-
<
|
| 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 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
<
|
| 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 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
<
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
// Show selected tab
|
| 531 |
-
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 532 |
-
event.target.classList.add('active');
|
| 533 |
-
|
| 534 |
-
// Load data for tab
|
| 535 |
-
switch(tabName) {
|
| 536 |
-
case 'status':
|
| 537 |
-
loadGlobalStatus();
|
| 538 |
-
break;
|
| 539 |
-
case 'providers':
|
| 540 |
-
loadProviders();
|
| 541 |
-
break;
|
| 542 |
-
case 'market':
|
| 543 |
-
loadMarketData();
|
| 544 |
-
loadSentiment();
|
| 545 |
-
loadTrending();
|
| 546 |
-
break;
|
| 547 |
-
case 'apl':
|
| 548 |
-
loadAPLSummary();
|
| 549 |
-
break;
|
| 550 |
-
case 'hf-models':
|
| 551 |
-
loadHFModels();
|
| 552 |
-
loadHFHealth();
|
| 553 |
-
break;
|
| 554 |
-
case 'diagnostics':
|
| 555 |
-
loadLastDiagnostics();
|
| 556 |
-
break;
|
| 557 |
-
case 'logs':
|
| 558 |
-
loadRecentLogs();
|
| 559 |
-
break;
|
| 560 |
-
}
|
| 561 |
-
}
|
| 562 |
-
|
| 563 |
-
// Global Status
|
| 564 |
-
async function loadGlobalStatus() {
|
| 565 |
-
try {
|
| 566 |
-
const [status, stats] = await Promise.all([
|
| 567 |
-
apiClient.get('/api/status'),
|
| 568 |
-
apiClient.get('/api/stats')
|
| 569 |
-
]);
|
| 570 |
-
|
| 571 |
-
document.getElementById('system-health').textContent = status.system_health.toUpperCase();
|
| 572 |
-
document.getElementById('total-providers').textContent = status.total_providers;
|
| 573 |
-
document.getElementById('validated-providers').textContent = status.validated_providers;
|
| 574 |
-
|
| 575 |
-
// Quick market view
|
| 576 |
-
const market = await apiClient.get('/api/market');
|
| 577 |
-
let marketHTML = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">';
|
| 578 |
-
market.cryptocurrencies.forEach(coin => {
|
| 579 |
-
const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
|
| 580 |
-
marketHTML += `
|
| 581 |
-
<div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
|
| 582 |
-
<div style="font-weight: 600;">${coin.name} (${coin.symbol})</div>
|
| 583 |
-
<div style="font-size: 24px; margin: 10px 0;">$${coin.price.toLocaleString()}</div>
|
| 584 |
-
<div class="${changeClass}">${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%</div>
|
| 585 |
-
</div>
|
| 586 |
-
`;
|
| 587 |
-
});
|
| 588 |
-
marketHTML += '</div>';
|
| 589 |
-
document.getElementById('quick-market-view').innerHTML = marketHTML;
|
| 590 |
-
|
| 591 |
-
} catch (error) {
|
| 592 |
-
console.error('Error loading global status:', error);
|
| 593 |
-
showError('Failed to load global status');
|
| 594 |
-
}
|
| 595 |
-
}
|
| 596 |
-
|
| 597 |
-
// Providers
|
| 598 |
-
async function loadProviders() {
|
| 599 |
-
try {
|
| 600 |
-
const response = await apiClient.get('/api/providers');
|
| 601 |
-
const providers = response.providers;
|
| 602 |
-
|
| 603 |
-
if (providers.length === 0) {
|
| 604 |
-
document.getElementById('providers-table').innerHTML = '<div class="empty-state">No providers found. Run APL scan to discover providers.</div>';
|
| 605 |
-
return;
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
let html = `
|
| 609 |
-
<table>
|
| 610 |
-
<thead>
|
| 611 |
-
<tr>
|
| 612 |
-
<th>Provider ID</th>
|
| 613 |
-
<th>Name</th>
|
| 614 |
-
<th>Category</th>
|
| 615 |
-
<th>Type</th>
|
| 616 |
-
<th>Status</th>
|
| 617 |
-
<th>Response Time</th>
|
| 618 |
-
</tr>
|
| 619 |
-
</thead>
|
| 620 |
-
<tbody>
|
| 621 |
-
`;
|
| 622 |
-
|
| 623 |
-
providers.forEach(p => {
|
| 624 |
-
const statusClass = p.status === 'validated' ? 'status-online' : 'status-degraded';
|
| 625 |
-
html += `
|
| 626 |
-
<tr>
|
| 627 |
-
<td><code>${p.provider_id}</code></td>
|
| 628 |
-
<td>${p.name}</td>
|
| 629 |
-
<td>${p.category}</td>
|
| 630 |
-
<td>${p.type}</td>
|
| 631 |
-
<td class="${statusClass}">${p.status}</td>
|
| 632 |
-
<td>${p.response_time_ms ? p.response_time_ms.toFixed(0) + 'ms' : 'N/A'}</td>
|
| 633 |
-
</tr>
|
| 634 |
-
`;
|
| 635 |
-
});
|
| 636 |
-
|
| 637 |
-
html += '</tbody></table>';
|
| 638 |
-
document.getElementById('providers-table').innerHTML = html;
|
| 639 |
-
|
| 640 |
-
} catch (error) {
|
| 641 |
-
console.error('Error loading providers:', error);
|
| 642 |
-
showError('Failed to load providers');
|
| 643 |
-
}
|
| 644 |
-
}
|
| 645 |
-
|
| 646 |
-
function filterProviders() {
|
| 647 |
-
// Would filter the providers table
|
| 648 |
-
loadProviders();
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
// Market Data
|
| 652 |
-
async function loadMarketData() {
|
| 653 |
-
try {
|
| 654 |
-
const data = await apiClient.get('/api/market');
|
| 655 |
-
|
| 656 |
-
let html = '<table><thead><tr><th>Rank</th><th>Coin</th><th>Price</th><th>24h Change</th><th>Market Cap</th><th>Volume 24h</th></tr></thead><tbody>';
|
| 657 |
-
|
| 658 |
-
data.cryptocurrencies.forEach(coin => {
|
| 659 |
-
const changeClass = coin.change_24h >= 0 ? 'status-online' : 'status-offline';
|
| 660 |
-
html += `
|
| 661 |
-
<tr>
|
| 662 |
-
<td>${coin.rank}</td>
|
| 663 |
-
<td><strong>${coin.name}</strong> (${coin.symbol})</td>
|
| 664 |
-
<td>$${coin.price.toLocaleString()}</td>
|
| 665 |
-
<td class="${changeClass}">${coin.change_24h >= 0 ? '+' : ''}${coin.change_24h.toFixed(2)}%</td>
|
| 666 |
-
<td>$${(coin.market_cap / 1e9).toFixed(2)}B</td>
|
| 667 |
-
<td>$${(coin.volume_24h / 1e9).toFixed(2)}B</td>
|
| 668 |
-
</tr>
|
| 669 |
-
`;
|
| 670 |
-
});
|
| 671 |
-
|
| 672 |
-
html += '</tbody></table>';
|
| 673 |
-
html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
|
| 674 |
-
|
| 675 |
-
document.getElementById('market-data-container').innerHTML = html;
|
| 676 |
-
} catch (error) {
|
| 677 |
-
console.error('Error loading market data:', error);
|
| 678 |
-
document.getElementById('market-data-container').innerHTML = '<div class="error-message">Failed to load market data: ' + error.message + '</div>';
|
| 679 |
-
}
|
| 680 |
-
}
|
| 681 |
-
|
| 682 |
-
async function loadSentiment() {
|
| 683 |
-
try {
|
| 684 |
-
const data = await apiClient.get('/api/sentiment');
|
| 685 |
-
|
| 686 |
-
const fngValue = data.fear_greed_index;
|
| 687 |
-
let color = '--success';
|
| 688 |
-
if (fngValue < 30) color = '--danger';
|
| 689 |
-
else if (fngValue < 50) color = '--warning';
|
| 690 |
-
|
| 691 |
-
document.getElementById('sentiment-data').innerHTML = `
|
| 692 |
-
<div style="text-align: center; padding: 20px;">
|
| 693 |
-
<div style="font-size: 64px; color: var(${color}); font-weight: 700;">${fngValue}</div>
|
| 694 |
-
<div style="font-size: 24px; margin: 10px 0;">${data.fear_greed_label}</div>
|
| 695 |
-
<p style="color: var(--text-muted);">Source: ${data.source}</p>
|
| 696 |
-
</div>
|
| 697 |
-
`;
|
| 698 |
-
} catch (error) {
|
| 699 |
-
console.error('Error loading sentiment:', error);
|
| 700 |
-
document.getElementById('sentiment-data').innerHTML = '<div class="error-message">Failed to load sentiment: ' + error.message + '</div>';
|
| 701 |
-
}
|
| 702 |
-
}
|
| 703 |
-
|
| 704 |
-
async function loadTrending() {
|
| 705 |
-
try {
|
| 706 |
-
const data = await apiClient.get('/api/trending');
|
| 707 |
-
|
| 708 |
-
if (data.trending.length === 0) {
|
| 709 |
-
document.getElementById('trending-coins').innerHTML = '<div class="empty-state">No trending coins available</div>';
|
| 710 |
-
return;
|
| 711 |
-
}
|
| 712 |
-
|
| 713 |
-
let html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px;">';
|
| 714 |
-
data.trending.forEach(coin => {
|
| 715 |
-
html += `
|
| 716 |
-
<div style="background: var(--bg-dark); padding: 15px; border-radius: 8px;">
|
| 717 |
-
<div style="font-weight: 600;">${coin.name}</div>
|
| 718 |
-
<div style="color: var(--text-muted);">${coin.symbol}</div>
|
| 719 |
-
${coin.market_cap_rank ? `<div style="margin-top: 10px;">Rank: #${coin.market_cap_rank}</div>` : ''}
|
| 720 |
-
</div>
|
| 721 |
-
`;
|
| 722 |
-
});
|
| 723 |
-
html += '</div>';
|
| 724 |
-
html += `<p style="margin-top: 15px; color: var(--text-muted);">Source: ${data.source}</p>`;
|
| 725 |
-
|
| 726 |
-
document.getElementById('trending-coins').innerHTML = html;
|
| 727 |
-
} catch (error) {
|
| 728 |
-
console.error('Error loading trending:', error);
|
| 729 |
-
document.getElementById('trending-coins').innerHTML = '<div class="error-message">Failed to load trending: ' + error.message + '</div>';
|
| 730 |
-
}
|
| 731 |
-
}
|
| 732 |
-
|
| 733 |
-
// APL Functions
|
| 734 |
-
async function runAPL() {
|
| 735 |
-
const btn = document.getElementById('apl-run-btn');
|
| 736 |
-
btn.disabled = true;
|
| 737 |
-
btn.textContent = '⏳ Running APL Scan...';
|
| 738 |
-
|
| 739 |
-
document.getElementById('apl-status').innerHTML = '<div class="loading">Running APL scan... This may take 1-2 minutes...</div>';
|
| 740 |
-
document.getElementById('apl-output').textContent = 'Executing APL scan...';
|
| 741 |
-
|
| 742 |
-
try {
|
| 743 |
-
const result = await apiClient.post('/api/apl/run');
|
| 744 |
-
|
| 745 |
-
if (result.status === 'completed') {
|
| 746 |
-
document.getElementById('apl-status').innerHTML = `
|
| 747 |
-
<div class="success-message">
|
| 748 |
-
✓ APL scan completed successfully!<br>
|
| 749 |
-
Providers count: ${result.providers_count}<br>
|
| 750 |
-
Time: ${result.timestamp}
|
| 751 |
-
</div>
|
| 752 |
-
`;
|
| 753 |
-
document.getElementById('apl-output').textContent = result.stdout || 'Scan completed.';
|
| 754 |
-
|
| 755 |
-
// Reload summary
|
| 756 |
-
await loadAPLSummary();
|
| 757 |
-
|
| 758 |
-
} else {
|
| 759 |
-
document.getElementById('apl-status').innerHTML = `<div class="error-message">APL scan ${result.status}: ${result.message || 'Unknown error'}</div>`;
|
| 760 |
-
document.getElementById('apl-output').textContent = result.stdout || 'No output';
|
| 761 |
-
}
|
| 762 |
-
} catch (error) {
|
| 763 |
-
console.error('Error running APL:', error);
|
| 764 |
-
document.getElementById('apl-status').innerHTML = '<div class="error-message">Failed to run APL: ' + error.message + '</div>';
|
| 765 |
-
} finally {
|
| 766 |
-
btn.disabled = false;
|
| 767 |
-
btn.textContent = '🤖 Run APL Scan';
|
| 768 |
-
}
|
| 769 |
-
}
|
| 770 |
-
|
| 771 |
-
async function loadAPLSummary() {
|
| 772 |
-
try {
|
| 773 |
-
const summary = await apiClient.get('/api/apl/summary');
|
| 774 |
-
|
| 775 |
-
if (summary.status === 'not_available') {
|
| 776 |
-
document.getElementById('apl-summary').innerHTML = '<div class="empty-state">No APL report available. Run APL scan first.</div>';
|
| 777 |
-
return;
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
document.getElementById('apl-summary').innerHTML = `
|
| 781 |
-
<div class="stats-grid">
|
| 782 |
-
<div class="stat-card">
|
| 783 |
-
<div class="label">HTTP Candidates</div>
|
| 784 |
-
<div class="value">${summary.http_candidates}</div>
|
| 785 |
-
<span class="badge badge-success">Valid: ${summary.http_valid}</span>
|
| 786 |
-
</div>
|
| 787 |
-
<div class="stat-card">
|
| 788 |
-
<div class="label">HTTP Invalid</div>
|
| 789 |
-
<div class="value">${summary.http_invalid}</div>
|
| 790 |
-
<span class="badge badge-warning">Conditional: ${summary.http_conditional}</span>
|
| 791 |
-
</div>
|
| 792 |
-
<div class="stat-card">
|
| 793 |
-
<div class="label">HF Models</div>
|
| 794 |
-
<div class="value">${summary.hf_candidates}</div>
|
| 795 |
-
<span class="badge badge-success">Valid: ${summary.hf_valid}</span>
|
| 796 |
-
</div>
|
| 797 |
-
<div class="stat-card">
|
| 798 |
-
<div class="label">Total Active</div>
|
| 799 |
-
<div class="value">${summary.total_active}</div>
|
| 800 |
-
<span class="badge badge-success">Providers</span>
|
| 801 |
-
</div>
|
| 802 |
-
</div>
|
| 803 |
-
<p style="margin-top: 15px; color: var(--text-muted);">Last updated: ${summary.timestamp}</p>
|
| 804 |
-
`;
|
| 805 |
-
} catch (error) {
|
| 806 |
-
console.error('Error loading APL summary:', error);
|
| 807 |
-
document.getElementById('apl-summary').innerHTML = '<div class="error-message">Failed to load APL summary</div>';
|
| 808 |
-
}
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
async function loadAPLReport() {
|
| 812 |
-
try {
|
| 813 |
-
const report = await apiClient.get('/api/apl/report');
|
| 814 |
-
|
| 815 |
-
if (report.status === 'not_available') {
|
| 816 |
-
showError('APL report not available. Run APL scan first.');
|
| 817 |
-
return;
|
| 818 |
-
}
|
| 819 |
-
|
| 820 |
-
document.getElementById('apl-output').textContent = JSON.stringify(report, null, 2);
|
| 821 |
-
} catch (error) {
|
| 822 |
-
console.error('Error loading APL report:', error);
|
| 823 |
-
showError('Failed to load APL report');
|
| 824 |
-
}
|
| 825 |
-
}
|
| 826 |
-
|
| 827 |
-
// HF Models
|
| 828 |
-
async function loadHFModels() {
|
| 829 |
-
try {
|
| 830 |
-
const response = await apiClient.get('/api/hf/models');
|
| 831 |
-
|
| 832 |
-
if (response.count === 0) {
|
| 833 |
-
document.getElementById('hf-models-container').innerHTML = '<div class="empty-state">No HF models found. Run APL scan to discover models.</div>';
|
| 834 |
-
return;
|
| 835 |
-
}
|
| 836 |
-
|
| 837 |
-
let html = '';
|
| 838 |
-
response.models.forEach(model => {
|
| 839 |
-
const statusClass = model.status === 'VALID' ? 'valid' : model.status === 'CONDITIONALLY_AVAILABLE' ? 'conditional' : 'invalid';
|
| 840 |
-
html += `
|
| 841 |
-
<div class="model-card ${statusClass}">
|
| 842 |
-
<div style="font-weight: 600; margin-bottom: 5px;">${model.provider_name}</div>
|
| 843 |
-
<div style="color: var(--text-muted); font-size: 13px;">${model.provider_id}</div>
|
| 844 |
-
<div style="margin-top: 10px;">
|
| 845 |
-
<span class="badge badge-${statusClass === 'valid' ? 'success' : statusClass === 'conditional' ? 'warning' : 'danger'}">${model.status}</span>
|
| 846 |
-
</div>
|
| 847 |
-
${model.error_reason ? `<div style="margin-top: 10px; color: var(--warning);">${model.error_reason}</div>` : ''}
|
| 848 |
-
</div>
|
| 849 |
-
`;
|
| 850 |
-
});
|
| 851 |
-
|
| 852 |
-
document.getElementById('hf-models-container').innerHTML = html;
|
| 853 |
-
} catch (error) {
|
| 854 |
-
console.error('Error loading HF models:', error);
|
| 855 |
-
document.getElementById('hf-models-container').innerHTML = '<div class="error-message">Failed to load HF models</div>';
|
| 856 |
-
}
|
| 857 |
-
}
|
| 858 |
-
|
| 859 |
-
async function loadHFHealth() {
|
| 860 |
-
try {
|
| 861 |
-
const health = await apiClient.get('/api/hf/health');
|
| 862 |
-
|
| 863 |
-
const statusClass = health.ok ? 'status-online' : 'status-offline';
|
| 864 |
-
document.getElementById('hf-health').innerHTML = `
|
| 865 |
-
<div style="padding: 20px; background: var(--bg-dark); border-radius: 8px;">
|
| 866 |
-
<div class="${statusClass}" style="font-size: 24px; font-weight: 700;">${health.ok ? '✓ Healthy' : '✗ Unhealthy'}</div>
|
| 867 |
-
${health.ok ? `
|
| 868 |
-
<div style="margin-top: 15px;">
|
| 869 |
-
<div>Models: ${health.counts.models}</div>
|
| 870 |
-
<div>Datasets: ${health.counts.datasets}</div>
|
| 871 |
-
<div>Last refresh: ${new Date(health.last_refresh_epoch * 1000).toLocaleString()}</div>
|
| 872 |
-
</div>
|
| 873 |
-
` : `
|
| 874 |
-
<div style="margin-top: 15px; color: var(--danger);">Error: ${health.error || health.fail_reason}</div>
|
| 875 |
-
`}
|
| 876 |
-
</div>
|
| 877 |
-
`;
|
| 878 |
-
} catch (error) {
|
| 879 |
-
console.error('Error loading HF health:', error);
|
| 880 |
-
document.getElementById('hf-health').innerHTML = '<div class="error-message">Failed to load HF health</div>';
|
| 881 |
-
}
|
| 882 |
-
}
|
| 883 |
-
|
| 884 |
-
// Diagnostics
|
| 885 |
-
async function runDiagnostics(autoFix = false) {
|
| 886 |
-
try {
|
| 887 |
-
const result = await apiClient.post('/api/diagnostics/run?auto_fix=' + autoFix);
|
| 888 |
-
|
| 889 |
-
let html = `
|
| 890 |
-
<div class="success-message">
|
| 891 |
-
✓ Diagnostics completed<br>
|
| 892 |
-
Issues found: ${result.issues_found}<br>
|
| 893 |
-
Time: ${result.timestamp}
|
| 894 |
-
</div>
|
| 895 |
-
`;
|
| 896 |
-
|
| 897 |
-
if (result.issues.length > 0) {
|
| 898 |
-
html += '<div style="margin-top: 20px;"><h4>Issues Found:</h4>';
|
| 899 |
-
result.issues.forEach(issue => {
|
| 900 |
-
html += `<div class="log-entry error">${issue.type}: ${issue.message}</div>`;
|
| 901 |
-
});
|
| 902 |
-
html += '</div>';
|
| 903 |
-
}
|
| 904 |
-
|
| 905 |
-
if (result.fixes_applied.length > 0) {
|
| 906 |
-
html += '<div style="margin-top: 20px;"><h4>Fixes Applied:</h4>';
|
| 907 |
-
result.fixes_applied.forEach(fix => {
|
| 908 |
-
html += `<div class="log-entry">${fix}</div>`;
|
| 909 |
-
});
|
| 910 |
-
html += '</div>';
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
document.getElementById('diagnostics-results').innerHTML = html;
|
| 914 |
-
} catch (error) {
|
| 915 |
-
console.error('Error running diagnostics:', error);
|
| 916 |
-
showError('Failed to run diagnostics');
|
| 917 |
-
}
|
| 918 |
-
}
|
| 919 |
-
|
| 920 |
-
async function loadLastDiagnostics() {
|
| 921 |
-
try {
|
| 922 |
-
const result = await apiClient.get('/api/diagnostics/last');
|
| 923 |
-
|
| 924 |
-
if (result.status === 'no_previous_run') {
|
| 925 |
-
document.getElementById('diagnostics-results').innerHTML = '<div class="empty-state">No previous diagnostics run found</div>';
|
| 926 |
-
return;
|
| 927 |
-
}
|
| 928 |
-
|
| 929 |
-
// Display last diagnostics
|
| 930 |
-
document.getElementById('diagnostics-results').innerHTML = `<pre>${JSON.stringify(result, null, 2)}</pre>`;
|
| 931 |
-
} catch (error) {
|
| 932 |
-
console.error('Error loading diagnostics:', error);
|
| 933 |
-
showError('Failed to load diagnostics');
|
| 934 |
-
}
|
| 935 |
-
}
|
| 936 |
-
|
| 937 |
-
// Logs
|
| 938 |
-
async function loadRecentLogs() {
|
| 939 |
-
try {
|
| 940 |
-
const response = await apiClient.get('/api/logs/recent');
|
| 941 |
-
|
| 942 |
-
if (response.count === 0) {
|
| 943 |
-
document.getElementById('logs-container').innerHTML = '<div class="empty-state">No logs available</div>';
|
| 944 |
-
return;
|
| 945 |
-
}
|
| 946 |
-
|
| 947 |
-
let html = '';
|
| 948 |
-
response.logs.forEach(log => {
|
| 949 |
-
const className = log.level === 'ERROR' ? 'error' : '';
|
| 950 |
-
html += `
|
| 951 |
-
<div class="log-entry ${className}">
|
| 952 |
-
<div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
|
| 953 |
-
<div>${log.message || JSON.stringify(log)}</div>
|
| 954 |
-
</div>
|
| 955 |
-
`;
|
| 956 |
-
});
|
| 957 |
-
|
| 958 |
-
document.getElementById('logs-container').innerHTML = html;
|
| 959 |
-
} catch (error) {
|
| 960 |
-
console.error('Error loading logs:', error);
|
| 961 |
-
document.getElementById('logs-container').innerHTML = '<div class="error-message">Failed to load logs</div>';
|
| 962 |
-
}
|
| 963 |
-
}
|
| 964 |
-
|
| 965 |
-
async function loadErrorLogs() {
|
| 966 |
-
try {
|
| 967 |
-
const response = await apiClient.get('/api/logs/errors');
|
| 968 |
-
|
| 969 |
-
if (response.count === 0) {
|
| 970 |
-
document.getElementById('logs-container').innerHTML = '<div class="empty-state">No error logs found</div>';
|
| 971 |
-
return;
|
| 972 |
-
}
|
| 973 |
-
|
| 974 |
-
let html = '';
|
| 975 |
-
response.errors.forEach(log => {
|
| 976 |
-
html += `
|
| 977 |
-
<div class="log-entry error">
|
| 978 |
-
<div class="log-timestamp">${log.timestamp || new Date().toISOString()}</div>
|
| 979 |
-
<div>${log.message || JSON.stringify(log)}</div>
|
| 980 |
-
</div>
|
| 981 |
-
`;
|
| 982 |
-
});
|
| 983 |
-
|
| 984 |
-
document.getElementById('logs-container').innerHTML = html;
|
| 985 |
-
} catch (error) {
|
| 986 |
-
console.error('Error loading error logs:', error);
|
| 987 |
-
showError('Failed to load error logs');
|
| 988 |
-
}
|
| 989 |
-
}
|
| 990 |
-
|
| 991 |
-
// Utility functions
|
| 992 |
-
function showError(message) {
|
| 993 |
-
alert('Error: ' + message);
|
| 994 |
-
}
|
| 995 |
-
|
| 996 |
-
function refreshAllData() {
|
| 997 |
-
loadGlobalStatus();
|
| 998 |
-
loadProviders();
|
| 999 |
-
loadAPLSummary();
|
| 1000 |
-
}
|
| 1001 |
-
|
| 1002 |
-
// Auto-refresh every 30 seconds
|
| 1003 |
-
setInterval(() => {
|
| 1004 |
-
const activeTab = document.querySelector('.tab-content.active').id;
|
| 1005 |
-
if (activeTab === 'tab-status') {
|
| 1006 |
-
loadGlobalStatus();
|
| 1007 |
-
}
|
| 1008 |
-
}, 30000);
|
| 1009 |
-
|
| 1010 |
-
// Load initial data
|
| 1011 |
-
window.addEventListener('DOMContentLoaded', () => {
|
| 1012 |
-
console.log('✓ Admin Dashboard Loaded - Real Data Only');
|
| 1013 |
-
loadGlobalStatus();
|
| 1014 |
-
});
|
| 1015 |
-
</script>
|
| 1016 |
</body>
|
| 1017 |
-
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Crypto Intelligence Admin</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
| 10 |
+
<link rel="stylesheet" href="/static/css/unified-ui.css" />
|
| 11 |
+
<link rel="stylesheet" href="/static/css/components.css" />
|
| 12 |
+
<script defer src="/static/js/ui-feedback.js"></script>
|
| 13 |
+
<script defer src="/static/js/admin-app.js"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
</head>
|
| 15 |
+
<body class="page page-admin">
|
| 16 |
+
<header class="top-nav">
|
| 17 |
+
<div class="branding">
|
| 18 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M4 12h16M12 4v16"/></svg>
|
| 19 |
+
<div>
|
| 20 |
+
<strong>Providers & Scheduling</strong>
|
| 21 |
+
<small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/providers · /api/logs</small>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<nav class="nav-links">
|
| 25 |
+
<a href="/dashboard">Dashboard</a>
|
| 26 |
+
<a class="active" href="/admin">Admin</a>
|
| 27 |
+
<a href="/hf_console">HF Console</a>
|
| 28 |
+
<a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
|
| 29 |
+
</nav>
|
| 30 |
+
</header>
|
| 31 |
+
|
| 32 |
+
<main class="page-content">
|
| 33 |
+
<section class="card">
|
| 34 |
+
<div class="section-heading">
|
| 35 |
+
<h2>Providers Health</h2>
|
| 36 |
+
<span class="badge info" id="providers-count">Loading...</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
</div>
|
| 38 |
+
<div class="table-card">
|
| 39 |
+
<table>
|
| 40 |
+
<thead>
|
| 41 |
+
<tr><th>Provider</th><th>Status</th><th>Response (ms)</th><th>Category</th></tr>
|
| 42 |
+
</thead>
|
| 43 |
+
<tbody id="providers-table">
|
| 44 |
+
<tr><td colspan="4">Loading providers...</td></tr>
|
| 45 |
+
</tbody>
|
| 46 |
+
</table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
</div>
|
| 48 |
+
</section>
|
| 49 |
|
| 50 |
+
<section class="split-grid">
|
| 51 |
+
<article class="card" id="provider-detail">
|
| 52 |
+
<div class="section-heading">
|
| 53 |
+
<h2>Provider Detail</h2>
|
| 54 |
+
<span class="badge info" id="selected-provider">Select a provider</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
</div>
|
| 56 |
+
<ul class="list" id="provider-detail-list"></ul>
|
| 57 |
+
</article>
|
| 58 |
+
<article class="card">
|
| 59 |
+
<div class="section-heading">
|
| 60 |
+
<h2>Configuration Snapshot</h2>
|
| 61 |
+
<span class="badge info" id="config-summary">Loading...</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
</div>
|
| 63 |
+
<ul class="list" id="config-list"></ul>
|
| 64 |
+
</article>
|
| 65 |
+
</section>
|
| 66 |
+
|
| 67 |
+
<section class="split-grid">
|
| 68 |
+
<article class="card">
|
| 69 |
+
<div class="section-heading"><h2>Logs ( /api/logs )</h2><span class="badge info">Latest</span></div>
|
| 70 |
+
<div id="logs-list" class="ws-stream"></div>
|
| 71 |
+
</article>
|
| 72 |
+
<article class="card">
|
| 73 |
+
<div class="section-heading"><h2>Alerts ( /api/alerts )</h2><span class="badge info">Live</span></div>
|
| 74 |
+
<div id="alerts-list"></div>
|
| 75 |
+
</article>
|
| 76 |
+
</section>
|
| 77 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
</body>
|
| 79 |
+
</html>
|
ai_models.py
CHANGED
|
@@ -8,6 +8,28 @@ from dataclasses import dataclass
|
|
| 8 |
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
| 9 |
from config import HUGGINGFACE_MODELS, get_settings
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
try:
|
| 12 |
from transformers import pipeline
|
| 13 |
TRANSFORMERS_AVAILABLE = True
|
|
@@ -17,18 +39,38 @@ except ImportError:
|
|
| 17 |
logger = logging.getLogger(__name__)
|
| 18 |
settings = get_settings()
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
]
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
| 26 |
"svalabs/twitter-xlm-roberta-bitcoin-sentiment",
|
| 27 |
-
"mayurjadhav/crypto-sentiment-model"
|
|
|
|
|
|
|
|
|
|
| 28 |
]
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
@dataclass(frozen=True)
|
| 34 |
class PipelineSpec:
|
|
@@ -50,26 +92,29 @@ for lk in ["sentiment_twitter", "sentiment_financial", "summarization", "crypto_
|
|
| 50 |
category="legacy"
|
| 51 |
)
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
|
| 55 |
MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
|
| 56 |
key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
|
| 57 |
category="crypto_sentiment", requires_auth=("ElKulako" in mid)
|
| 58 |
)
|
| 59 |
|
| 60 |
-
# Social
|
| 61 |
for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
|
| 62 |
MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
|
| 63 |
key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
|
| 64 |
)
|
| 65 |
|
| 66 |
-
# Financial
|
| 67 |
for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
|
| 68 |
MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
|
| 69 |
key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
|
| 70 |
)
|
| 71 |
|
| 72 |
-
# News
|
| 73 |
for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
|
| 74 |
MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
|
| 75 |
key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
|
|
@@ -97,44 +142,118 @@ class ModelRegistry:
|
|
| 97 |
if key in self._pipelines:
|
| 98 |
return self._pipelines[key]
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
try:
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
except Exception as e:
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
return self._pipelines[key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
def initialize_models(self):
|
| 111 |
if self._initialized:
|
| 112 |
-
return {"status": "already_initialized", "models_loaded": len(self._pipelines)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if not TRANSFORMERS_AVAILABLE:
|
| 114 |
-
return {"status": "transformers_not_available", "models_loaded": 0}
|
| 115 |
|
| 116 |
loaded, failed = [], []
|
| 117 |
-
|
|
|
|
|
|
|
| 118 |
try:
|
| 119 |
self.get_pipeline(key)
|
| 120 |
loaded.append(key)
|
|
|
|
|
|
|
| 121 |
except Exception as e:
|
| 122 |
-
|
|
|
|
| 123 |
|
| 124 |
self._initialized = True
|
| 125 |
-
|
|
|
|
| 126 |
|
| 127 |
_registry = ModelRegistry()
|
| 128 |
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
|
| 131 |
def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
|
| 132 |
-
if not TRANSFORMERS_AVAILABLE:
|
| 133 |
-
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "transformers N/A"}
|
| 134 |
|
| 135 |
results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
|
| 136 |
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
try:
|
| 139 |
pipe = _registry.get_pipeline(key)
|
| 140 |
res = pipe(text[:512])
|
|
@@ -145,15 +264,20 @@ def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
|
|
| 145 |
|
| 146 |
mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
|
| 147 |
|
| 148 |
-
spec = MODEL_SPECS
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
| 150 |
labels_count[mapped] += 1
|
| 151 |
total_conf += score
|
|
|
|
|
|
|
| 152 |
except Exception as e:
|
| 153 |
logger.warning(f"Ensemble failed for {key}: {e}")
|
| 154 |
|
| 155 |
if not results:
|
| 156 |
-
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0}
|
| 157 |
|
| 158 |
final = max(labels_count, key=labels_count.get)
|
| 159 |
avg_conf = total_conf / len(results)
|
|
@@ -211,16 +335,11 @@ def analyze_news_item(item: Dict[str, Any]):
|
|
| 211 |
def get_model_info():
|
| 212 |
return {
|
| 213 |
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 214 |
-
"
|
|
|
|
| 215 |
"models_initialized": _registry._initialized,
|
| 216 |
"models_loaded": len(_registry._pipelines),
|
| 217 |
-
"
|
| 218 |
-
"crypto_sentiment": CRYPTO_SENTIMENT_MODELS,
|
| 219 |
-
"social_sentiment": SOCIAL_SENTIMENT_MODELS,
|
| 220 |
-
"financial_sentiment": FINANCIAL_SENTIMENT_MODELS,
|
| 221 |
-
"news_sentiment": NEWS_SENTIMENT_MODELS,
|
| 222 |
-
"decision": DECISION_MODELS
|
| 223 |
-
},
|
| 224 |
"total_models": len(MODEL_SPECS)
|
| 225 |
}
|
| 226 |
|
|
|
|
| 8 |
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
| 9 |
from config import HUGGINGFACE_MODELS, get_settings
|
| 10 |
|
| 11 |
+
# Set environment variables to avoid TensorFlow/Keras issues
|
| 12 |
+
# We'll force PyTorch framework instead
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
|
| 16 |
+
# Completely disable TensorFlow to force PyTorch
|
| 17 |
+
os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
|
| 18 |
+
os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
|
| 19 |
+
os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3')
|
| 20 |
+
os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
|
| 21 |
+
|
| 22 |
+
# Mock tf_keras to prevent transformers from trying to import it
|
| 23 |
+
# This prevents the broken tf-keras installation from causing errors
|
| 24 |
+
class TfKerasMock:
|
| 25 |
+
"""Mock tf_keras to prevent import errors when transformers checks for TensorFlow"""
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
# Add mock to sys.modules before transformers imports
|
| 29 |
+
sys.modules['tf_keras'] = TfKerasMock()
|
| 30 |
+
sys.modules['tf_keras.src'] = TfKerasMock()
|
| 31 |
+
sys.modules['tf_keras.src.utils'] = TfKerasMock()
|
| 32 |
+
|
| 33 |
try:
|
| 34 |
from transformers import pipeline
|
| 35 |
TRANSFORMERS_AVAILABLE = True
|
|
|
|
| 39 |
logger = logging.getLogger(__name__)
|
| 40 |
settings = get_settings()
|
| 41 |
|
| 42 |
+
HF_MODE = os.getenv("HF_MODE", "off").lower()
|
| 43 |
+
HF_TOKEN_ENV = os.getenv("HF_TOKEN")
|
| 44 |
+
|
| 45 |
+
if HF_MODE not in ("off", "public", "auth"):
|
| 46 |
+
HF_MODE = "off"
|
| 47 |
+
logger.warning(f"Invalid HF_MODE, defaulting to 'off'")
|
| 48 |
+
|
| 49 |
+
if HF_MODE == "auth" and not HF_TOKEN_ENV:
|
| 50 |
+
HF_MODE = "off"
|
| 51 |
+
logger.warning("HF_MODE='auth' but HF_TOKEN not set, defaulting to 'off'")
|
| 52 |
+
|
| 53 |
+
ACTIVE_MODELS = [
|
| 54 |
+
"ElKulako/cryptobert",
|
| 55 |
+
"kk08/CryptoBERT",
|
| 56 |
+
"ProsusAI/finbert"
|
| 57 |
]
|
| 58 |
+
|
| 59 |
+
LEGACY_MODELS = [
|
| 60 |
+
"burakutf/finetuned-finbert-crypto",
|
| 61 |
+
"mathugo/crypto_news_bert",
|
| 62 |
"svalabs/twitter-xlm-roberta-bitcoin-sentiment",
|
| 63 |
+
"mayurjadhav/crypto-sentiment-model",
|
| 64 |
+
"cardiffnlp/twitter-roberta-base-sentiment",
|
| 65 |
+
"mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis",
|
| 66 |
+
"agarkovv/CryptoTrader-LM"
|
| 67 |
]
|
| 68 |
+
|
| 69 |
+
CRYPTO_SENTIMENT_MODELS = ACTIVE_MODELS[:2] + LEGACY_MODELS[:2]
|
| 70 |
+
SOCIAL_SENTIMENT_MODELS = LEGACY_MODELS[2:4]
|
| 71 |
+
FINANCIAL_SENTIMENT_MODELS = [ACTIVE_MODELS[2]] + [LEGACY_MODELS[4]]
|
| 72 |
+
NEWS_SENTIMENT_MODELS = [LEGACY_MODELS[5]]
|
| 73 |
+
DECISION_MODELS = [LEGACY_MODELS[6]]
|
| 74 |
|
| 75 |
@dataclass(frozen=True)
|
| 76 |
class PipelineSpec:
|
|
|
|
| 92 |
category="legacy"
|
| 93 |
)
|
| 94 |
|
| 95 |
+
for i, mid in enumerate(ACTIVE_MODELS):
|
| 96 |
+
MODEL_SPECS[f"active_{i}"] = PipelineSpec(
|
| 97 |
+
key=f"active_{i}", task="sentiment-analysis", model_id=mid,
|
| 98 |
+
category="crypto_sentiment" if i < 2 else "financial_sentiment",
|
| 99 |
+
requires_auth=("ElKulako" in mid)
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
for i, mid in enumerate(CRYPTO_SENTIMENT_MODELS):
|
| 103 |
MODEL_SPECS[f"crypto_sent_{i}"] = PipelineSpec(
|
| 104 |
key=f"crypto_sent_{i}", task="sentiment-analysis", model_id=mid,
|
| 105 |
category="crypto_sentiment", requires_auth=("ElKulako" in mid)
|
| 106 |
)
|
| 107 |
|
|
|
|
| 108 |
for i, mid in enumerate(SOCIAL_SENTIMENT_MODELS):
|
| 109 |
MODEL_SPECS[f"social_sent_{i}"] = PipelineSpec(
|
| 110 |
key=f"social_sent_{i}", task="sentiment-analysis", model_id=mid, category="social_sentiment"
|
| 111 |
)
|
| 112 |
|
|
|
|
| 113 |
for i, mid in enumerate(FINANCIAL_SENTIMENT_MODELS):
|
| 114 |
MODEL_SPECS[f"financial_sent_{i}"] = PipelineSpec(
|
| 115 |
key=f"financial_sent_{i}", task="sentiment-analysis", model_id=mid, category="financial_sentiment"
|
| 116 |
)
|
| 117 |
|
|
|
|
| 118 |
for i, mid in enumerate(NEWS_SENTIMENT_MODELS):
|
| 119 |
MODEL_SPECS[f"news_sent_{i}"] = PipelineSpec(
|
| 120 |
key=f"news_sent_{i}", task="sentiment-analysis", model_id=mid, category="news_sentiment"
|
|
|
|
| 142 |
if key in self._pipelines:
|
| 143 |
return self._pipelines[key]
|
| 144 |
|
| 145 |
+
if HF_MODE == "off":
|
| 146 |
+
raise ModelNotAvailable("HF_MODE=off")
|
| 147 |
+
|
| 148 |
+
token_value = None
|
| 149 |
+
if HF_MODE == "auth":
|
| 150 |
+
token_value = HF_TOKEN_ENV or settings.hf_token
|
| 151 |
+
elif HF_MODE == "public":
|
| 152 |
+
token_value = None
|
| 153 |
+
|
| 154 |
+
if spec.requires_auth and not token_value:
|
| 155 |
+
raise ModelNotAvailable("Model requires auth but no token available")
|
| 156 |
+
|
| 157 |
+
logger.info(f"Loading model: {spec.model_id} (mode: {HF_MODE})")
|
| 158 |
try:
|
| 159 |
+
pipeline_kwargs = {
|
| 160 |
+
'task': spec.task,
|
| 161 |
+
'model': spec.model_id,
|
| 162 |
+
'tokenizer': spec.model_id,
|
| 163 |
+
'framework': 'pt',
|
| 164 |
+
'device': -1,
|
| 165 |
+
}
|
| 166 |
+
pipeline_kwargs['token'] = token_value
|
| 167 |
+
|
| 168 |
+
self._pipelines[key] = pipeline(**pipeline_kwargs)
|
| 169 |
except Exception as e:
|
| 170 |
+
error_msg = str(e)
|
| 171 |
+
error_lower = error_msg.lower()
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
from huggingface_hub.errors import RepositoryNotFoundError, HfHubHTTPError
|
| 175 |
+
hf_errors = (RepositoryNotFoundError, HfHubHTTPError)
|
| 176 |
+
except ImportError:
|
| 177 |
+
hf_errors = ()
|
| 178 |
+
|
| 179 |
+
is_auth_error = any(kw in error_lower for kw in ['401', 'unauthorized', 'repository not found', 'expired', 'token'])
|
| 180 |
+
is_hf_error = isinstance(e, hf_errors) or is_auth_error
|
| 181 |
+
|
| 182 |
+
if is_hf_error:
|
| 183 |
+
logger.warning(f"HF error for {spec.model_id}: {type(e).__name__}")
|
| 184 |
+
raise ModelNotAvailable(f"HF error: {spec.model_id}") from e
|
| 185 |
+
|
| 186 |
+
if any(kw in error_lower for kw in ['keras', 'tensorflow', 'tf_keras', 'framework']):
|
| 187 |
+
try:
|
| 188 |
+
pipeline_kwargs['torch_dtype'] = 'float32'
|
| 189 |
+
self._pipelines[key] = pipeline(**pipeline_kwargs)
|
| 190 |
+
return self._pipelines[key]
|
| 191 |
+
except Exception:
|
| 192 |
+
raise ModelNotAvailable(f"Framework error: {spec.model_id}") from e
|
| 193 |
+
|
| 194 |
+
raise ModelNotAvailable(f"Load failed: {spec.model_id}") from e
|
| 195 |
|
| 196 |
return self._pipelines[key]
|
| 197 |
+
|
| 198 |
+
def get_loaded_models(self):
|
| 199 |
+
"""Get list of all loaded model keys"""
|
| 200 |
+
return list(self._pipelines.keys())
|
| 201 |
+
|
| 202 |
+
def get_available_sentiment_models(self):
|
| 203 |
+
"""Get list of all available sentiment model keys"""
|
| 204 |
+
return [key for key in MODEL_SPECS.keys() if "sent" in key or "sentiment" in key]
|
| 205 |
|
| 206 |
def initialize_models(self):
|
| 207 |
if self._initialized:
|
| 208 |
+
return {"status": "already_initialized", "mode": HF_MODE, "models_loaded": len(self._pipelines)}
|
| 209 |
+
|
| 210 |
+
if HF_MODE == "off":
|
| 211 |
+
self._initialized = True
|
| 212 |
+
return {"status": "disabled", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
|
| 213 |
+
|
| 214 |
if not TRANSFORMERS_AVAILABLE:
|
| 215 |
+
return {"status": "transformers_not_available", "mode": HF_MODE, "models_loaded": 0}
|
| 216 |
|
| 217 |
loaded, failed = [], []
|
| 218 |
+
active_keys = [f"active_{i}" for i in range(len(ACTIVE_MODELS))]
|
| 219 |
+
|
| 220 |
+
for key in active_keys:
|
| 221 |
try:
|
| 222 |
self.get_pipeline(key)
|
| 223 |
loaded.append(key)
|
| 224 |
+
except ModelNotAvailable as e:
|
| 225 |
+
failed.append((key, str(e)[:100]))
|
| 226 |
except Exception as e:
|
| 227 |
+
error_msg = str(e)[:100]
|
| 228 |
+
failed.append((key, error_msg))
|
| 229 |
|
| 230 |
self._initialized = True
|
| 231 |
+
status = "initialized" if loaded else "partial"
|
| 232 |
+
return {"status": status, "mode": HF_MODE, "models_loaded": len(loaded), "loaded": loaded, "failed": failed}
|
| 233 |
|
| 234 |
_registry = ModelRegistry()
|
| 235 |
|
| 236 |
+
AI_MODELS_SUMMARY = {"status": "not_initialized", "mode": "off", "models_loaded": 0, "loaded": [], "failed": []}
|
| 237 |
+
|
| 238 |
+
def initialize_models():
|
| 239 |
+
global AI_MODELS_SUMMARY
|
| 240 |
+
result = _registry.initialize_models()
|
| 241 |
+
AI_MODELS_SUMMARY = result
|
| 242 |
+
return result
|
| 243 |
|
| 244 |
def ensemble_crypto_sentiment(text: str) -> Dict[str, Any]:
|
| 245 |
+
if not TRANSFORMERS_AVAILABLE or HF_MODE == "off":
|
| 246 |
+
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "HF disabled" if HF_MODE == "off" else "transformers N/A"}
|
| 247 |
|
| 248 |
results, labels_count, total_conf = {}, {"bullish": 0, "bearish": 0, "neutral": 0}, 0.0
|
| 249 |
|
| 250 |
+
loaded_keys = _registry.get_loaded_models()
|
| 251 |
+
available_keys = [key for key in loaded_keys if "sent" in key or "sentiment" in key or key.startswith("active_")]
|
| 252 |
+
|
| 253 |
+
if not available_keys:
|
| 254 |
+
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "No models loaded"}
|
| 255 |
+
|
| 256 |
+
for key in available_keys:
|
| 257 |
try:
|
| 258 |
pipe = _registry.get_pipeline(key)
|
| 259 |
res = pipe(text[:512])
|
|
|
|
| 264 |
|
| 265 |
mapped = "bullish" if "POSITIVE" in label or "BULLISH" in label else ("bearish" if "NEGATIVE" in label or "BEARISH" in label else "neutral")
|
| 266 |
|
| 267 |
+
spec = MODEL_SPECS.get(key)
|
| 268 |
+
if spec:
|
| 269 |
+
results[spec.model_id] = {"label": mapped, "score": score}
|
| 270 |
+
else:
|
| 271 |
+
results[key] = {"label": mapped, "score": score}
|
| 272 |
labels_count[mapped] += 1
|
| 273 |
total_conf += score
|
| 274 |
+
except ModelNotAvailable:
|
| 275 |
+
continue
|
| 276 |
except Exception as e:
|
| 277 |
logger.warning(f"Ensemble failed for {key}: {e}")
|
| 278 |
|
| 279 |
if not results:
|
| 280 |
+
return {"label": "neutral", "confidence": 0.0, "scores": {}, "model_count": 0, "error": "All models failed"}
|
| 281 |
|
| 282 |
final = max(labels_count, key=labels_count.get)
|
| 283 |
avg_conf = total_conf / len(results)
|
|
|
|
| 335 |
def get_model_info():
|
| 336 |
return {
|
| 337 |
"transformers_available": TRANSFORMERS_AVAILABLE,
|
| 338 |
+
"hf_mode": HF_MODE,
|
| 339 |
+
"hf_token_configured": bool(HF_TOKEN_ENV or settings.hf_token) if HF_MODE == "auth" else False,
|
| 340 |
"models_initialized": _registry._initialized,
|
| 341 |
"models_loaded": len(_registry._pipelines),
|
| 342 |
+
"active_models": ACTIVE_MODELS,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
"total_models": len(MODEL_SPECS)
|
| 344 |
}
|
| 345 |
|
api-resources/crypto_resources_unified_2025-11-11.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app.js
CHANGED
|
@@ -133,8 +133,41 @@ class WebSocketClient {
|
|
| 133 |
}
|
| 134 |
|
| 135 |
handleError(error) {
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
this.updateStatus('error');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
handleClose() {
|
|
|
|
| 133 |
}
|
| 134 |
|
| 135 |
handleError(error) {
|
| 136 |
+
// WebSocket error events don't provide detailed error info
|
| 137 |
+
// Check socket state to provide better error context
|
| 138 |
+
const socketState = this.socket ? this.socket.readyState : 'null';
|
| 139 |
+
const stateNames = {
|
| 140 |
+
0: 'CONNECTING',
|
| 141 |
+
1: 'OPEN',
|
| 142 |
+
2: 'CLOSING',
|
| 143 |
+
3: 'CLOSED'
|
| 144 |
+
};
|
| 145 |
+
|
| 146 |
+
const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`;
|
| 147 |
+
|
| 148 |
+
// Only log error once to prevent spam
|
| 149 |
+
if (!this._errorLogged) {
|
| 150 |
+
console.error('[WS] Connection error:', {
|
| 151 |
+
url: this.url,
|
| 152 |
+
state: stateName,
|
| 153 |
+
readyState: socketState,
|
| 154 |
+
message: 'WebSocket connection failed. Check if server is running and URL is correct.'
|
| 155 |
+
});
|
| 156 |
+
this._errorLogged = true;
|
| 157 |
+
|
| 158 |
+
// Reset error flag after a delay to allow logging if error persists
|
| 159 |
+
setTimeout(() => {
|
| 160 |
+
this._errorLogged = false;
|
| 161 |
+
}, 5000);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
this.updateStatus('error');
|
| 165 |
+
|
| 166 |
+
// Attempt reconnection if not already scheduled
|
| 167 |
+
if (this.socket && this.socket.readyState === WebSocket.CLOSED &&
|
| 168 |
+
this.reconnectAttempts < this.maxReconnectAttempts) {
|
| 169 |
+
this.scheduleReconnect();
|
| 170 |
+
}
|
| 171 |
}
|
| 172 |
|
| 173 |
handleClose() {
|
app.py
CHANGED
|
@@ -1194,6 +1194,37 @@ demo = build_interface()
|
|
| 1194 |
if __name__ == "__main__":
|
| 1195 |
logger.info("Launching Gradio dashboard...")
|
| 1196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
demo.launch(
|
| 1198 |
server_name="0.0.0.0",
|
| 1199 |
server_port=7860,
|
|
|
|
| 1194 |
if __name__ == "__main__":
|
| 1195 |
logger.info("Launching Gradio dashboard...")
|
| 1196 |
|
| 1197 |
+
# Try to mount FastAPI app for API endpoints
|
| 1198 |
+
try:
|
| 1199 |
+
from fastapi import FastAPI as FastAPIApp
|
| 1200 |
+
from fastapi.middleware.wsgi import WSGIMiddleware
|
| 1201 |
+
import uvicorn
|
| 1202 |
+
from threading import Thread
|
| 1203 |
+
import time
|
| 1204 |
+
|
| 1205 |
+
# Import the FastAPI app from hf_unified_server
|
| 1206 |
+
try:
|
| 1207 |
+
from hf_unified_server import app as fastapi_app
|
| 1208 |
+
logger.info("✅ FastAPI app imported successfully")
|
| 1209 |
+
|
| 1210 |
+
# Start FastAPI server in a separate thread on port 7861
|
| 1211 |
+
def run_fastapi():
|
| 1212 |
+
uvicorn.run(
|
| 1213 |
+
fastapi_app,
|
| 1214 |
+
host="0.0.0.0",
|
| 1215 |
+
port=7861,
|
| 1216 |
+
log_level="info"
|
| 1217 |
+
)
|
| 1218 |
+
|
| 1219 |
+
fastapi_thread = Thread(target=run_fastapi, daemon=True)
|
| 1220 |
+
fastapi_thread.start()
|
| 1221 |
+
time.sleep(2) # Give FastAPI time to start
|
| 1222 |
+
logger.info("✅ FastAPI server started on port 7861")
|
| 1223 |
+
except ImportError as e:
|
| 1224 |
+
logger.warning(f"⚠️ Could not import FastAPI app: {e}")
|
| 1225 |
+
except Exception as e:
|
| 1226 |
+
logger.warning(f"⚠️ Could not start FastAPI server: {e}")
|
| 1227 |
+
|
| 1228 |
demo.launch(
|
| 1229 |
server_name="0.0.0.0",
|
| 1230 |
server_port=7860,
|
archive_html/admin_advanced.html
ADDED
|
@@ -0,0 +1,1862 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Advanced Admin Dashboard - Crypto Monitor</title>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 11 |
+
|
| 12 |
+
:root {
|
| 13 |
+
--primary: #6366f1;
|
| 14 |
+
--primary-dark: #4f46e5;
|
| 15 |
+
--primary-glow: rgba(99, 102, 241, 0.4);
|
| 16 |
+
--success: #10b981;
|
| 17 |
+
--warning: #f59e0b;
|
| 18 |
+
--danger: #ef4444;
|
| 19 |
+
--info: #3b82f6;
|
| 20 |
+
--bg-dark: #0f172a;
|
| 21 |
+
--bg-card: rgba(30, 41, 59, 0.7);
|
| 22 |
+
--bg-glass: rgba(30, 41, 59, 0.5);
|
| 23 |
+
--bg-hover: rgba(51, 65, 85, 0.8);
|
| 24 |
+
--text-light: #f1f5f9;
|
| 25 |
+
--text-muted: #94a3b8;
|
| 26 |
+
--border: rgba(51, 65, 85, 0.6);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 31 |
+
background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 50%, #000000 100%);
|
| 32 |
+
color: var(--text-light);
|
| 33 |
+
line-height: 1.6;
|
| 34 |
+
min-height: 100vh;
|
| 35 |
+
position: relative;
|
| 36 |
+
overflow-x: hidden;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/* Animated Background Particles */
|
| 40 |
+
body::before {
|
| 41 |
+
content: '';
|
| 42 |
+
position: fixed;
|
| 43 |
+
top: 0;
|
| 44 |
+
left: 0;
|
| 45 |
+
width: 100%;
|
| 46 |
+
height: 100%;
|
| 47 |
+
background:
|
| 48 |
+
radial-gradient(circle at 20% 50%, rgba(99, 102, 241, 0.1) 0%, transparent 50%),
|
| 49 |
+
radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
|
| 50 |
+
radial-gradient(circle at 40% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%);
|
| 51 |
+
animation: float 20s ease-in-out infinite;
|
| 52 |
+
pointer-events: none;
|
| 53 |
+
z-index: 0;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
@keyframes float {
|
| 57 |
+
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
| 58 |
+
33% { transform: translate(30px, -30px) rotate(120deg); }
|
| 59 |
+
66% { transform: translate(-20px, 20px) rotate(240deg); }
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.container {
|
| 63 |
+
max-width: 1800px;
|
| 64 |
+
margin: 0 auto;
|
| 65 |
+
padding: 20px;
|
| 66 |
+
position: relative;
|
| 67 |
+
z-index: 1;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Glassmorphic Header with Glow */
|
| 71 |
+
header {
|
| 72 |
+
background: linear-gradient(135deg, rgba(99, 102, 241, 0.9) 0%, rgba(79, 70, 229, 0.9) 100%);
|
| 73 |
+
backdrop-filter: blur(20px);
|
| 74 |
+
-webkit-backdrop-filter: blur(20px);
|
| 75 |
+
padding: 30px;
|
| 76 |
+
border-radius: 20px;
|
| 77 |
+
margin-bottom: 30px;
|
| 78 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 79 |
+
box-shadow:
|
| 80 |
+
0 8px 32px rgba(0, 0, 0, 0.3),
|
| 81 |
+
0 0 60px var(--primary-glow),
|
| 82 |
+
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
| 83 |
+
position: relative;
|
| 84 |
+
overflow: hidden;
|
| 85 |
+
animation: headerGlow 3s ease-in-out infinite alternate;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
@keyframes headerGlow {
|
| 89 |
+
0% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 40px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2); }
|
| 90 |
+
100% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 80px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3); }
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
header::before {
|
| 94 |
+
content: '';
|
| 95 |
+
position: absolute;
|
| 96 |
+
top: -50%;
|
| 97 |
+
left: -50%;
|
| 98 |
+
width: 200%;
|
| 99 |
+
height: 200%;
|
| 100 |
+
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
| 101 |
+
transform: rotate(45deg);
|
| 102 |
+
animation: headerShine 3s linear infinite;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@keyframes headerShine {
|
| 106 |
+
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
|
| 107 |
+
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
header h1 {
|
| 111 |
+
font-size: 36px;
|
| 112 |
+
font-weight: 700;
|
| 113 |
+
margin-bottom: 8px;
|
| 114 |
+
display: flex;
|
| 115 |
+
align-items: center;
|
| 116 |
+
gap: 15px;
|
| 117 |
+
position: relative;
|
| 118 |
+
z-index: 1;
|
| 119 |
+
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
header .icon {
|
| 123 |
+
font-size: 42px;
|
| 124 |
+
filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.5));
|
| 125 |
+
animation: iconPulse 2s ease-in-out infinite;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
@keyframes iconPulse {
|
| 129 |
+
0%, 100% { transform: scale(1); }
|
| 130 |
+
50% { transform: scale(1.1); }
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
header .subtitle {
|
| 134 |
+
color: rgba(255, 255, 255, 0.95);
|
| 135 |
+
font-size: 16px;
|
| 136 |
+
position: relative;
|
| 137 |
+
z-index: 1;
|
| 138 |
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* Glassmorphic Tabs */
|
| 142 |
+
.tabs {
|
| 143 |
+
display: flex;
|
| 144 |
+
gap: 10px;
|
| 145 |
+
margin-bottom: 30px;
|
| 146 |
+
flex-wrap: wrap;
|
| 147 |
+
background: var(--bg-glass);
|
| 148 |
+
backdrop-filter: blur(10px);
|
| 149 |
+
-webkit-backdrop-filter: blur(10px);
|
| 150 |
+
padding: 15px;
|
| 151 |
+
border-radius: 16px;
|
| 152 |
+
border: 1px solid var(--border);
|
| 153 |
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.tab-btn {
|
| 157 |
+
padding: 12px 24px;
|
| 158 |
+
background: rgba(255, 255, 255, 0.05);
|
| 159 |
+
backdrop-filter: blur(10px);
|
| 160 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 161 |
+
border-radius: 10px;
|
| 162 |
+
cursor: pointer;
|
| 163 |
+
font-weight: 600;
|
| 164 |
+
color: var(--text-light);
|
| 165 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 166 |
+
position: relative;
|
| 167 |
+
overflow: hidden;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.tab-btn::before {
|
| 171 |
+
content: '';
|
| 172 |
+
position: absolute;
|
| 173 |
+
top: 0;
|
| 174 |
+
left: -100%;
|
| 175 |
+
width: 100%;
|
| 176 |
+
height: 100%;
|
| 177 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
| 178 |
+
transition: left 0.5s;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.tab-btn:hover::before {
|
| 182 |
+
left: 100%;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.tab-btn:hover {
|
| 186 |
+
background: rgba(99, 102, 241, 0.2);
|
| 187 |
+
border-color: var(--primary);
|
| 188 |
+
transform: translateY(-2px);
|
| 189 |
+
box-shadow: 0 4px 12px var(--primary-glow);
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.tab-btn.active {
|
| 193 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
| 194 |
+
border-color: var(--primary);
|
| 195 |
+
box-shadow: 0 4px 20px var(--primary-glow);
|
| 196 |
+
transform: scale(1.05);
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.tab-content {
|
| 200 |
+
display: none;
|
| 201 |
+
animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.tab-content.active {
|
| 205 |
+
display: block;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
@keyframes fadeInUp {
|
| 209 |
+
from {
|
| 210 |
+
opacity: 0;
|
| 211 |
+
transform: translateY(20px);
|
| 212 |
+
}
|
| 213 |
+
to {
|
| 214 |
+
opacity: 1;
|
| 215 |
+
transform: translateY(0);
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* Glassmorphic Cards */
|
| 220 |
+
.card {
|
| 221 |
+
background: var(--bg-glass);
|
| 222 |
+
backdrop-filter: blur(10px);
|
| 223 |
+
-webkit-backdrop-filter: blur(10px);
|
| 224 |
+
border-radius: 16px;
|
| 225 |
+
padding: 24px;
|
| 226 |
+
margin-bottom: 20px;
|
| 227 |
+
border: 1px solid var(--border);
|
| 228 |
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
| 229 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.card:hover {
|
| 233 |
+
transform: translateY(-2px);
|
| 234 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 235 |
+
border-color: rgba(99, 102, 241, 0.3);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.card h3 {
|
| 239 |
+
color: var(--primary);
|
| 240 |
+
margin-bottom: 20px;
|
| 241 |
+
font-size: 20px;
|
| 242 |
+
display: flex;
|
| 243 |
+
align-items: center;
|
| 244 |
+
gap: 10px;
|
| 245 |
+
text-shadow: 0 0 20px var(--primary-glow);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* Animated Stat Cards */
|
| 249 |
+
.stats-grid {
|
| 250 |
+
display: grid;
|
| 251 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 252 |
+
gap: 20px;
|
| 253 |
+
margin-bottom: 30px;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.stat-card {
|
| 257 |
+
background: var(--bg-glass);
|
| 258 |
+
backdrop-filter: blur(10px);
|
| 259 |
+
-webkit-backdrop-filter: blur(10px);
|
| 260 |
+
padding: 24px;
|
| 261 |
+
border-radius: 16px;
|
| 262 |
+
border: 1px solid var(--border);
|
| 263 |
+
position: relative;
|
| 264 |
+
overflow: hidden;
|
| 265 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 266 |
+
animation: statCardIn 0.5s ease-out backwards;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
@keyframes statCardIn {
|
| 270 |
+
from {
|
| 271 |
+
opacity: 0;
|
| 272 |
+
transform: scale(0.9) translateY(20px);
|
| 273 |
+
}
|
| 274 |
+
to {
|
| 275 |
+
opacity: 1;
|
| 276 |
+
transform: scale(1) translateY(0);
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.stat-card:nth-child(1) { animation-delay: 0.1s; }
|
| 281 |
+
.stat-card:nth-child(2) { animation-delay: 0.2s; }
|
| 282 |
+
.stat-card:nth-child(3) { animation-delay: 0.3s; }
|
| 283 |
+
.stat-card:nth-child(4) { animation-delay: 0.4s; }
|
| 284 |
+
|
| 285 |
+
.stat-card::before {
|
| 286 |
+
content: '';
|
| 287 |
+
position: absolute;
|
| 288 |
+
top: 0;
|
| 289 |
+
left: 0;
|
| 290 |
+
right: 0;
|
| 291 |
+
height: 3px;
|
| 292 |
+
background: linear-gradient(90deg, var(--primary), var(--info), var(--success));
|
| 293 |
+
background-size: 200% 100%;
|
| 294 |
+
animation: gradientMove 3s ease infinite;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
@keyframes gradientMove {
|
| 298 |
+
0%, 100% { background-position: 0% 50%; }
|
| 299 |
+
50% { background-position: 100% 50%; }
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.stat-card:hover {
|
| 303 |
+
transform: translateY(-8px) scale(1.02);
|
| 304 |
+
box-shadow: 0 12px 40px rgba(99, 102, 241, 0.3);
|
| 305 |
+
border-color: var(--primary);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.stat-card .label {
|
| 309 |
+
color: var(--text-muted);
|
| 310 |
+
font-size: 13px;
|
| 311 |
+
text-transform: uppercase;
|
| 312 |
+
letter-spacing: 0.5px;
|
| 313 |
+
font-weight: 600;
|
| 314 |
+
margin-bottom: 8px;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.stat-card .value {
|
| 318 |
+
font-size: 42px;
|
| 319 |
+
font-weight: 700;
|
| 320 |
+
margin: 8px 0;
|
| 321 |
+
color: var(--primary);
|
| 322 |
+
text-shadow: 0 0 30px var(--primary-glow);
|
| 323 |
+
animation: valueCount 1s ease-out;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
@keyframes valueCount {
|
| 327 |
+
from { opacity: 0; transform: translateY(-10px); }
|
| 328 |
+
to { opacity: 1; transform: translateY(0); }
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.stat-card .change {
|
| 332 |
+
font-size: 14px;
|
| 333 |
+
font-weight: 600;
|
| 334 |
+
display: flex;
|
| 335 |
+
align-items: center;
|
| 336 |
+
gap: 5px;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.stat-card .change.positive {
|
| 340 |
+
color: var(--success);
|
| 341 |
+
animation: bounce 1s ease-in-out infinite;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
@keyframes bounce {
|
| 345 |
+
0%, 100% { transform: translateY(0); }
|
| 346 |
+
50% { transform: translateY(-3px); }
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.stat-card .change.negative {
|
| 350 |
+
color: var(--danger);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
/* Glassmorphic Chart Container */
|
| 354 |
+
.chart-container {
|
| 355 |
+
background: rgba(15, 23, 42, 0.5);
|
| 356 |
+
backdrop-filter: blur(10px);
|
| 357 |
+
padding: 20px;
|
| 358 |
+
border-radius: 12px;
|
| 359 |
+
margin-bottom: 20px;
|
| 360 |
+
height: 400px;
|
| 361 |
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
| 362 |
+
box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2);
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/* Modern Buttons */
|
| 366 |
+
.btn {
|
| 367 |
+
padding: 12px 24px;
|
| 368 |
+
border: none;
|
| 369 |
+
border-radius: 10px;
|
| 370 |
+
cursor: pointer;
|
| 371 |
+
font-weight: 600;
|
| 372 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 373 |
+
margin-right: 10px;
|
| 374 |
+
margin-bottom: 10px;
|
| 375 |
+
display: inline-flex;
|
| 376 |
+
align-items: center;
|
| 377 |
+
gap: 8px;
|
| 378 |
+
position: relative;
|
| 379 |
+
overflow: hidden;
|
| 380 |
+
backdrop-filter: blur(10px);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.btn::before {
|
| 384 |
+
content: '';
|
| 385 |
+
position: absolute;
|
| 386 |
+
top: 50%;
|
| 387 |
+
left: 50%;
|
| 388 |
+
width: 0;
|
| 389 |
+
height: 0;
|
| 390 |
+
border-radius: 50%;
|
| 391 |
+
background: rgba(255, 255, 255, 0.2);
|
| 392 |
+
transform: translate(-50%, -50%);
|
| 393 |
+
transition: width 0.6s, height 0.6s;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.btn:hover::before {
|
| 397 |
+
width: 300px;
|
| 398 |
+
height: 300px;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.btn-primary {
|
| 402 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
| 403 |
+
color: white;
|
| 404 |
+
box-shadow: 0 4px 15px var(--primary-glow);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
.btn-primary:hover {
|
| 408 |
+
transform: translateY(-3px);
|
| 409 |
+
box-shadow: 0 8px 25px var(--primary-glow);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.btn-success {
|
| 413 |
+
background: linear-gradient(135deg, var(--success), #059669);
|
| 414 |
+
color: white;
|
| 415 |
+
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3);
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.btn-success:hover {
|
| 419 |
+
transform: translateY(-3px);
|
| 420 |
+
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.btn-warning {
|
| 424 |
+
background: linear-gradient(135deg, var(--warning), #d97706);
|
| 425 |
+
color: white;
|
| 426 |
+
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.3);
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.btn-danger {
|
| 430 |
+
background: linear-gradient(135deg, var(--danger), #dc2626);
|
| 431 |
+
color: white;
|
| 432 |
+
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3);
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.btn-secondary {
|
| 436 |
+
background: rgba(51, 65, 85, 0.6);
|
| 437 |
+
color: var(--text-light);
|
| 438 |
+
border: 1px solid var(--border);
|
| 439 |
+
backdrop-filter: blur(10px);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.btn:disabled {
|
| 443 |
+
opacity: 0.5;
|
| 444 |
+
cursor: not-allowed;
|
| 445 |
+
transform: none !important;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.btn:active {
|
| 449 |
+
transform: scale(0.95);
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
/* Animated Progress Bar */
|
| 453 |
+
.progress-bar {
|
| 454 |
+
background: rgba(15, 23, 42, 0.8);
|
| 455 |
+
backdrop-filter: blur(10px);
|
| 456 |
+
height: 12px;
|
| 457 |
+
border-radius: 20px;
|
| 458 |
+
overflow: hidden;
|
| 459 |
+
margin-top: 10px;
|
| 460 |
+
border: 1px solid rgba(99, 102, 241, 0.3);
|
| 461 |
+
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 462 |
+
position: relative;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.progress-bar::before {
|
| 466 |
+
content: '';
|
| 467 |
+
position: absolute;
|
| 468 |
+
top: 0;
|
| 469 |
+
left: -100%;
|
| 470 |
+
width: 100%;
|
| 471 |
+
height: 100%;
|
| 472 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
| 473 |
+
animation: progressShine 2s linear infinite;
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
@keyframes progressShine {
|
| 477 |
+
0% { left: -100%; }
|
| 478 |
+
100% { left: 200%; }
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.progress-bar-fill {
|
| 482 |
+
height: 100%;
|
| 483 |
+
background: linear-gradient(90deg, var(--primary), var(--info), var(--success));
|
| 484 |
+
background-size: 200% 100%;
|
| 485 |
+
animation: progressGradient 2s ease infinite;
|
| 486 |
+
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 487 |
+
box-shadow: 0 0 20px var(--primary-glow);
|
| 488 |
+
position: relative;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
@keyframes progressGradient {
|
| 492 |
+
0%, 100% { background-position: 0% 50%; }
|
| 493 |
+
50% { background-position: 100% 50%; }
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
/* Glassmorphic Table */
|
| 497 |
+
table {
|
| 498 |
+
width: 100%;
|
| 499 |
+
border-collapse: collapse;
|
| 500 |
+
margin-top: 15px;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
table thead {
|
| 504 |
+
background: rgba(15, 23, 42, 0.6);
|
| 505 |
+
backdrop-filter: blur(10px);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
table th {
|
| 509 |
+
padding: 16px;
|
| 510 |
+
text-align: left;
|
| 511 |
+
font-weight: 600;
|
| 512 |
+
font-size: 12px;
|
| 513 |
+
text-transform: uppercase;
|
| 514 |
+
color: var(--text-muted);
|
| 515 |
+
border-bottom: 2px solid var(--border);
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
table td {
|
| 519 |
+
padding: 16px;
|
| 520 |
+
border-top: 1px solid var(--border);
|
| 521 |
+
transition: all 0.2s;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
table tbody tr {
|
| 525 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
table tbody tr:hover {
|
| 529 |
+
background: var(--bg-hover);
|
| 530 |
+
backdrop-filter: blur(10px);
|
| 531 |
+
transform: scale(1.01);
|
| 532 |
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
/* Animated Resource Item */
|
| 536 |
+
.resource-item {
|
| 537 |
+
background: var(--bg-glass);
|
| 538 |
+
backdrop-filter: blur(10px);
|
| 539 |
+
padding: 16px;
|
| 540 |
+
border-radius: 12px;
|
| 541 |
+
margin-bottom: 12px;
|
| 542 |
+
border-left: 4px solid var(--primary);
|
| 543 |
+
display: flex;
|
| 544 |
+
justify-content: space-between;
|
| 545 |
+
align-items: center;
|
| 546 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 547 |
+
animation: slideIn 0.5s ease-out backwards;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
@keyframes slideIn {
|
| 551 |
+
from {
|
| 552 |
+
opacity: 0;
|
| 553 |
+
transform: translateX(-20px);
|
| 554 |
+
}
|
| 555 |
+
to {
|
| 556 |
+
opacity: 1;
|
| 557 |
+
transform: translateX(0);
|
| 558 |
+
}
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
.resource-item:hover {
|
| 562 |
+
transform: translateX(5px) scale(1.02);
|
| 563 |
+
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.resource-item.duplicate {
|
| 567 |
+
border-left-color: var(--warning);
|
| 568 |
+
background: rgba(245, 158, 11, 0.1);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.resource-item.error {
|
| 572 |
+
border-left-color: var(--danger);
|
| 573 |
+
background: rgba(239, 68, 68, 0.1);
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.resource-item.valid {
|
| 577 |
+
border-left-color: var(--success);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
/* Animated Badges */
|
| 581 |
+
.badge {
|
| 582 |
+
display: inline-block;
|
| 583 |
+
padding: 6px 12px;
|
| 584 |
+
border-radius: 20px;
|
| 585 |
+
font-size: 11px;
|
| 586 |
+
font-weight: 600;
|
| 587 |
+
text-transform: uppercase;
|
| 588 |
+
backdrop-filter: blur(10px);
|
| 589 |
+
animation: badgePulse 2s ease-in-out infinite;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
@keyframes badgePulse {
|
| 593 |
+
0%, 100% { transform: scale(1); }
|
| 594 |
+
50% { transform: scale(1.05); }
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.badge-success {
|
| 598 |
+
background: rgba(16, 185, 129, 0.3);
|
| 599 |
+
color: var(--success);
|
| 600 |
+
box-shadow: 0 0 15px rgba(16, 185, 129, 0.3);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.badge-warning {
|
| 604 |
+
background: rgba(245, 158, 11, 0.3);
|
| 605 |
+
color: var(--warning);
|
| 606 |
+
box-shadow: 0 0 15px rgba(245, 158, 11, 0.3);
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.badge-danger {
|
| 610 |
+
background: rgba(239, 68, 68, 0.3);
|
| 611 |
+
color: var(--danger);
|
| 612 |
+
box-shadow: 0 0 15px rgba(239, 68, 68, 0.3);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.badge-info {
|
| 616 |
+
background: rgba(59, 130, 246, 0.3);
|
| 617 |
+
color: var(--info);
|
| 618 |
+
box-shadow: 0 0 15px rgba(59, 130, 246, 0.3);
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
/* Search/Filter Glassmorphic */
|
| 622 |
+
.search-bar {
|
| 623 |
+
display: flex;
|
| 624 |
+
gap: 15px;
|
| 625 |
+
margin-bottom: 20px;
|
| 626 |
+
flex-wrap: wrap;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.search-bar input,
|
| 630 |
+
.search-bar select {
|
| 631 |
+
padding: 12px;
|
| 632 |
+
border-radius: 10px;
|
| 633 |
+
border: 1px solid var(--border);
|
| 634 |
+
background: rgba(15, 23, 42, 0.6);
|
| 635 |
+
backdrop-filter: blur(10px);
|
| 636 |
+
color: var(--text-light);
|
| 637 |
+
flex: 1;
|
| 638 |
+
min-width: 200px;
|
| 639 |
+
transition: all 0.3s;
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.search-bar input:focus,
|
| 643 |
+
.search-bar select:focus {
|
| 644 |
+
outline: none;
|
| 645 |
+
border-color: var(--primary);
|
| 646 |
+
box-shadow: 0 0 20px var(--primary-glow);
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
/* Loading Spinner with Glow */
|
| 650 |
+
.spinner {
|
| 651 |
+
border: 4px solid rgba(255, 255, 255, 0.1);
|
| 652 |
+
border-top-color: var(--primary);
|
| 653 |
+
border-radius: 50%;
|
| 654 |
+
width: 50px;
|
| 655 |
+
height: 50px;
|
| 656 |
+
animation: spin 0.8s linear infinite;
|
| 657 |
+
margin: 40px auto;
|
| 658 |
+
box-shadow: 0 0 30px var(--primary-glow);
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
@keyframes spin {
|
| 662 |
+
to { transform: rotate(360deg); }
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
/* Toast Notification with Glass */
|
| 666 |
+
.toast {
|
| 667 |
+
position: fixed;
|
| 668 |
+
bottom: 20px;
|
| 669 |
+
right: 20px;
|
| 670 |
+
background: var(--bg-glass);
|
| 671 |
+
backdrop-filter: blur(20px);
|
| 672 |
+
-webkit-backdrop-filter: blur(20px);
|
| 673 |
+
padding: 16px 24px;
|
| 674 |
+
border-radius: 12px;
|
| 675 |
+
border: 1px solid var(--border);
|
| 676 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
| 677 |
+
display: none;
|
| 678 |
+
align-items: center;
|
| 679 |
+
gap: 12px;
|
| 680 |
+
z-index: 1000;
|
| 681 |
+
animation: toastIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
@keyframes toastIn {
|
| 685 |
+
from {
|
| 686 |
+
transform: translateX(400px) scale(0.5);
|
| 687 |
+
opacity: 0;
|
| 688 |
+
}
|
| 689 |
+
to {
|
| 690 |
+
transform: translateX(0) scale(1);
|
| 691 |
+
opacity: 1;
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.toast.show {
|
| 696 |
+
display: flex;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
.toast.success {
|
| 700 |
+
border-left: 4px solid var(--success);
|
| 701 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(16, 185, 129, 0.3);
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
.toast.error {
|
| 705 |
+
border-left: 4px solid var(--danger);
|
| 706 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(239, 68, 68, 0.3);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
/* Modal with Glass */
|
| 710 |
+
.modal {
|
| 711 |
+
display: none;
|
| 712 |
+
position: fixed;
|
| 713 |
+
top: 0;
|
| 714 |
+
left: 0;
|
| 715 |
+
right: 0;
|
| 716 |
+
bottom: 0;
|
| 717 |
+
background: rgba(0, 0, 0, 0.8);
|
| 718 |
+
backdrop-filter: blur(10px);
|
| 719 |
+
z-index: 1000;
|
| 720 |
+
align-items: center;
|
| 721 |
+
justify-content: center;
|
| 722 |
+
animation: fadeIn 0.3s;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.modal.show {
|
| 726 |
+
display: flex;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
.modal-content {
|
| 730 |
+
background: var(--bg-glass);
|
| 731 |
+
backdrop-filter: blur(20px);
|
| 732 |
+
-webkit-backdrop-filter: blur(20px);
|
| 733 |
+
padding: 30px;
|
| 734 |
+
border-radius: 20px;
|
| 735 |
+
border: 1px solid var(--border);
|
| 736 |
+
max-width: 600px;
|
| 737 |
+
width: 90%;
|
| 738 |
+
max-height: 80vh;
|
| 739 |
+
overflow-y: auto;
|
| 740 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
| 741 |
+
animation: modalSlideIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
@keyframes modalSlideIn {
|
| 745 |
+
from {
|
| 746 |
+
transform: scale(0.5) translateY(-50px);
|
| 747 |
+
opacity: 0;
|
| 748 |
+
}
|
| 749 |
+
to {
|
| 750 |
+
transform: scale(1) translateY(0);
|
| 751 |
+
opacity: 1;
|
| 752 |
+
}
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.modal-content h2 {
|
| 756 |
+
margin-bottom: 20px;
|
| 757 |
+
color: var(--primary);
|
| 758 |
+
text-shadow: 0 0 20px var(--primary-glow);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.modal-content .form-group {
|
| 762 |
+
margin-bottom: 20px;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.modal-content label {
|
| 766 |
+
display: block;
|
| 767 |
+
margin-bottom: 8px;
|
| 768 |
+
font-weight: 600;
|
| 769 |
+
color: var(--text-muted);
|
| 770 |
+
}
|
| 771 |
+
|
| 772 |
+
.modal-content input,
|
| 773 |
+
.modal-content textarea,
|
| 774 |
+
.modal-content select {
|
| 775 |
+
width: 100%;
|
| 776 |
+
padding: 12px;
|
| 777 |
+
border-radius: 10px;
|
| 778 |
+
border: 1px solid var(--border);
|
| 779 |
+
background: rgba(15, 23, 42, 0.6);
|
| 780 |
+
backdrop-filter: blur(10px);
|
| 781 |
+
color: var(--text-light);
|
| 782 |
+
transition: all 0.3s;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
.modal-content input:focus,
|
| 786 |
+
.modal-content textarea:focus,
|
| 787 |
+
.modal-content select:focus {
|
| 788 |
+
outline: none;
|
| 789 |
+
border-color: var(--primary);
|
| 790 |
+
box-shadow: 0 0 20px var(--primary-glow);
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.modal-content textarea {
|
| 794 |
+
min-height: 100px;
|
| 795 |
+
resize: vertical;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
/* Grid Layout */
|
| 799 |
+
.grid-2 {
|
| 800 |
+
display: grid;
|
| 801 |
+
grid-template-columns: repeat(2, 1fr);
|
| 802 |
+
gap: 20px;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
@media (max-width: 1024px) {
|
| 806 |
+
.grid-2 {
|
| 807 |
+
grid-template-columns: 1fr;
|
| 808 |
+
}
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
@media (max-width: 768px) {
|
| 812 |
+
.stats-grid {
|
| 813 |
+
grid-template-columns: 1fr;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
header h1 {
|
| 817 |
+
font-size: 28px;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.tabs {
|
| 821 |
+
flex-direction: column;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.tab-btn {
|
| 825 |
+
width: 100%;
|
| 826 |
+
}
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
/* Scrollbar Styling */
|
| 830 |
+
::-webkit-scrollbar {
|
| 831 |
+
width: 10px;
|
| 832 |
+
height: 10px;
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
::-webkit-scrollbar-track {
|
| 836 |
+
background: rgba(15, 23, 42, 0.5);
|
| 837 |
+
border-radius: 10px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
::-webkit-scrollbar-thumb {
|
| 841 |
+
background: linear-gradient(135deg, var(--primary), var(--info));
|
| 842 |
+
border-radius: 10px;
|
| 843 |
+
box-shadow: 0 0 10px var(--primary-glow);
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
::-webkit-scrollbar-thumb:hover {
|
| 847 |
+
background: linear-gradient(135deg, var(--info), var(--success));
|
| 848 |
+
}
|
| 849 |
+
</style>
|
| 850 |
+
</head>
|
| 851 |
+
<body>
|
| 852 |
+
<div class="container">
|
| 853 |
+
<header>
|
| 854 |
+
<h1>
|
| 855 |
+
<span class="icon">📊</span>
|
| 856 |
+
Crypto Monitor Admin Dashboard
|
| 857 |
+
</h1>
|
| 858 |
+
<p class="subtitle">Real-time provider management & system monitoring | NO MOCK DATA</p>
|
| 859 |
+
</header>
|
| 860 |
+
|
| 861 |
+
<!-- Tabs -->
|
| 862 |
+
<div class="tabs">
|
| 863 |
+
<button class="tab-btn active" onclick="switchTab('dashboard')">📊 Dashboard</button>
|
| 864 |
+
<button class="tab-btn" onclick="switchTab('analytics')">📈 Analytics</button>
|
| 865 |
+
<button class="tab-btn" onclick="switchTab('resources')">🔧 Resource Manager</button>
|
| 866 |
+
<button class="tab-btn" onclick="switchTab('discovery')">🔍 Auto-Discovery</button>
|
| 867 |
+
<button class="tab-btn" onclick="switchTab('diagnostics')">🛠️ Diagnostics</button>
|
| 868 |
+
<button class="tab-btn" onclick="switchTab('logs')">📝 Logs</button>
|
| 869 |
+
</div>
|
| 870 |
+
|
| 871 |
+
<!-- Dashboard Tab -->
|
| 872 |
+
<div id="tab-dashboard" class="tab-content active">
|
| 873 |
+
<div class="stats-grid">
|
| 874 |
+
<div class="stat-card">
|
| 875 |
+
<div class="label">System Health</div>
|
| 876 |
+
<div class="value" id="system-health">HEALTHY</div>
|
| 877 |
+
<div class="change positive">✅ Healthy</div>
|
| 878 |
+
</div>
|
| 879 |
+
|
| 880 |
+
<div class="stat-card">
|
| 881 |
+
<div class="label">Total Providers</div>
|
| 882 |
+
<div class="value" id="total-providers">95</div>
|
| 883 |
+
<div class="change positive">↑ +12 this week</div>
|
| 884 |
+
</div>
|
| 885 |
+
|
| 886 |
+
<div class="stat-card">
|
| 887 |
+
<div class="label">Validated</div>
|
| 888 |
+
<div class="value" style="color: var(--success);" id="validated-count">32</div>
|
| 889 |
+
<div class="change positive">✓ All Active</div>
|
| 890 |
+
</div>
|
| 891 |
+
|
| 892 |
+
<div class="stat-card">
|
| 893 |
+
<div class="label">Database</div>
|
| 894 |
+
<div class="value">✓</div>
|
| 895 |
+
<div class="change positive">🗄️ Connected</div>
|
| 896 |
+
</div>
|
| 897 |
+
</div>
|
| 898 |
+
|
| 899 |
+
<div class="card">
|
| 900 |
+
<h3>⚡ Quick Actions</h3>
|
| 901 |
+
<button class="btn btn-primary" onclick="refreshAllData()">🔄 Refresh All</button>
|
| 902 |
+
<button class="btn btn-success" onclick="runAPLScan()">🤖 Run APL Scan</button>
|
| 903 |
+
<button class="btn btn-secondary" onclick="runDiagnostics(false)">🔧 Run Diagnostics</button>
|
| 904 |
+
</div>
|
| 905 |
+
|
| 906 |
+
<div class="card">
|
| 907 |
+
<h3>📊 Recent Market Data</h3>
|
| 908 |
+
<div class="progress-bar" style="margin-bottom: 20px;">
|
| 909 |
+
<div class="progress-bar-fill" style="width: 85%;"></div>
|
| 910 |
+
</div>
|
| 911 |
+
<div id="quick-market-view">Loading market data...</div>
|
| 912 |
+
</div>
|
| 913 |
+
|
| 914 |
+
<div class="grid-2">
|
| 915 |
+
<div class="card">
|
| 916 |
+
<h3>📈 Request Timeline (24h)</h3>
|
| 917 |
+
<div class="chart-container">
|
| 918 |
+
<canvas id="requestsChart"></canvas>
|
| 919 |
+
</div>
|
| 920 |
+
</div>
|
| 921 |
+
|
| 922 |
+
<div class="card">
|
| 923 |
+
<h3>🎯 Success vs Errors</h3>
|
| 924 |
+
<div class="chart-container">
|
| 925 |
+
<canvas id="statusChart"></canvas>
|
| 926 |
+
</div>
|
| 927 |
+
</div>
|
| 928 |
+
</div>
|
| 929 |
+
</div>
|
| 930 |
+
|
| 931 |
+
<!-- Analytics Tab -->
|
| 932 |
+
<div id="tab-analytics" class="tab-content">
|
| 933 |
+
<div class="card">
|
| 934 |
+
<h3>📈 Performance Analytics</h3>
|
| 935 |
+
<div class="search-bar">
|
| 936 |
+
<select id="analytics-timeframe">
|
| 937 |
+
<option value="1h">Last Hour</option>
|
| 938 |
+
<option value="24h" selected>Last 24 Hours</option>
|
| 939 |
+
<option value="7d">Last 7 Days</option>
|
| 940 |
+
<option value="30d">Last 30 Days</option>
|
| 941 |
+
</select>
|
| 942 |
+
<button class="btn btn-primary" onclick="refreshAnalytics()">🔄 Refresh</button>
|
| 943 |
+
<button class="btn btn-secondary" onclick="exportAnalytics()">📥 Export Data</button>
|
| 944 |
+
</div>
|
| 945 |
+
|
| 946 |
+
<div class="chart-container" style="height: 500px;">
|
| 947 |
+
<canvas id="performanceChart"></canvas>
|
| 948 |
+
</div>
|
| 949 |
+
</div>
|
| 950 |
+
|
| 951 |
+
<div class="grid-2">
|
| 952 |
+
<div class="card">
|
| 953 |
+
<h3>🏆 Top Performing Resources</h3>
|
| 954 |
+
<div id="top-resources">Loading...</div>
|
| 955 |
+
</div>
|
| 956 |
+
|
| 957 |
+
<div class="card">
|
| 958 |
+
<h3>⚠️ Resources with Issues</h3>
|
| 959 |
+
<div id="problem-resources">Loading...</div>
|
| 960 |
+
</div>
|
| 961 |
+
</div>
|
| 962 |
+
</div>
|
| 963 |
+
|
| 964 |
+
<!-- Resource Manager Tab -->
|
| 965 |
+
<div id="tab-resources" class="tab-content">
|
| 966 |
+
<div class="card">
|
| 967 |
+
<h3>🔧 Resource Management</h3>
|
| 968 |
+
|
| 969 |
+
<div class="search-bar">
|
| 970 |
+
<input type="text" id="resource-search" placeholder="🔍 Search resources..." oninput="filterResources()">
|
| 971 |
+
<select id="resource-filter" onchange="filterResources()">
|
| 972 |
+
<option value="all">All Resources</option>
|
| 973 |
+
<option value="valid">✅ Valid</option>
|
| 974 |
+
<option value="duplicate">⚠️ Duplicates</option>
|
| 975 |
+
<option value="error">❌ Errors</option>
|
| 976 |
+
<option value="hf-model">🤖 HF Models</option>
|
| 977 |
+
</select>
|
| 978 |
+
<button class="btn btn-primary" onclick="scanResources()">🔄 Scan All</button>
|
| 979 |
+
<button class="btn btn-success" onclick="openAddResourceModal()">➕ Add Resource</button>
|
| 980 |
+
</div>
|
| 981 |
+
|
| 982 |
+
<div class="card" style="background: rgba(245, 158, 11, 0.1); padding: 15px; margin-bottom: 20px;">
|
| 983 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 984 |
+
<div>
|
| 985 |
+
<strong>Duplicate Detection:</strong>
|
| 986 |
+
<span id="duplicate-count" class="badge badge-warning">0 found</span>
|
| 987 |
+
</div>
|
| 988 |
+
<button class="btn btn-warning" onclick="fixDuplicates()">🔧 Auto-Fix Duplicates</button>
|
| 989 |
+
</div>
|
| 990 |
+
</div>
|
| 991 |
+
|
| 992 |
+
<div id="resources-list">Loading resources...</div>
|
| 993 |
+
</div>
|
| 994 |
+
|
| 995 |
+
<div class="card">
|
| 996 |
+
<h3>🔄 Bulk Operations</h3>
|
| 997 |
+
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
| 998 |
+
<button class="btn btn-success" onclick="validateAllResources()">✅ Validate All</button>
|
| 999 |
+
<button class="btn btn-warning" onclick="refreshAllResources()">🔄 Refresh All</button>
|
| 1000 |
+
<button class="btn btn-danger" onclick="removeInvalidResources()">🗑️ Remove Invalid</button>
|
| 1001 |
+
<button class="btn btn-secondary" onclick="exportResources()">📥 Export Config</button>
|
| 1002 |
+
<button class="btn btn-secondary" onclick="importResources()">📤 Import Config</button>
|
| 1003 |
+
</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
</div>
|
| 1006 |
+
|
| 1007 |
+
<!-- Auto-Discovery Tab -->
|
| 1008 |
+
<div id="tab-discovery" class="tab-content">
|
| 1009 |
+
<div class="card">
|
| 1010 |
+
<h3>🔍 Auto-Discovery Engine</h3>
|
| 1011 |
+
<p style="color: var(--text-muted); margin-bottom: 20px;">
|
| 1012 |
+
Automatically discover, validate, and integrate new API providers and HuggingFace models.
|
| 1013 |
+
</p>
|
| 1014 |
+
|
| 1015 |
+
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
|
| 1016 |
+
<button class="btn btn-success" onclick="runFullDiscovery()" id="discovery-btn">
|
| 1017 |
+
🚀 Run Full Discovery
|
| 1018 |
+
</button>
|
| 1019 |
+
<button class="btn btn-primary" onclick="runAPLScan()">
|
| 1020 |
+
🤖 APL Scan
|
| 1021 |
+
</button>
|
| 1022 |
+
<button class="btn btn-secondary" onclick="discoverHFModels()">
|
| 1023 |
+
🧠 Discover HF Models
|
| 1024 |
+
</button>
|
| 1025 |
+
<button class="btn btn-secondary" onclick="discoverAPIs()">
|
| 1026 |
+
🌐 Discover APIs
|
| 1027 |
+
</button>
|
| 1028 |
+
</div>
|
| 1029 |
+
|
| 1030 |
+
<div id="discovery-progress" style="display: none;">
|
| 1031 |
+
<div style="display: flex; justify-content: space-between; margin-bottom: 10px;">
|
| 1032 |
+
<span>Discovery in progress...</span>
|
| 1033 |
+
<span id="discovery-percent">0%</span>
|
| 1034 |
+
</div>
|
| 1035 |
+
<div class="progress-bar">
|
| 1036 |
+
<div class="progress-bar-fill" id="discovery-progress-bar" style="width: 0%"></div>
|
| 1037 |
+
</div>
|
| 1038 |
+
</div>
|
| 1039 |
+
|
| 1040 |
+
<div id="discovery-results"></div>
|
| 1041 |
+
</div>
|
| 1042 |
+
|
| 1043 |
+
<div class="card">
|
| 1044 |
+
<h3>📊 Discovery Statistics</h3>
|
| 1045 |
+
<div class="stats-grid">
|
| 1046 |
+
<div class="stat-card">
|
| 1047 |
+
<div class="label">New Resources Found</div>
|
| 1048 |
+
<div class="value" id="discovery-found">0</div>
|
| 1049 |
+
</div>
|
| 1050 |
+
<div class="stat-card">
|
| 1051 |
+
<div class="label">Successfully Validated</div>
|
| 1052 |
+
<div class="value" id="discovery-validated" style="color: var(--success);">0</div>
|
| 1053 |
+
</div>
|
| 1054 |
+
<div class="stat-card">
|
| 1055 |
+
<div class="label">Failed Validation</div>
|
| 1056 |
+
<div class="value" id="discovery-failed" style="color: var(--danger);">0</div>
|
| 1057 |
+
</div>
|
| 1058 |
+
<div class="stat-card">
|
| 1059 |
+
<div class="label">Last Scan</div>
|
| 1060 |
+
<div class="value" id="discovery-last" style="font-size: 20px;">Never</div>
|
| 1061 |
+
</div>
|
| 1062 |
+
</div>
|
| 1063 |
+
</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
|
| 1066 |
+
<!-- Diagnostics Tab -->
|
| 1067 |
+
<div id="tab-diagnostics" class="tab-content">
|
| 1068 |
+
<div class="card">
|
| 1069 |
+
<h3>🛠️ System Diagnostics</h3>
|
| 1070 |
+
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
|
| 1071 |
+
<button class="btn btn-primary" onclick="runDiagnostics(false)">🔍 Scan Only</button>
|
| 1072 |
+
<button class="btn btn-success" onclick="runDiagnostics(true)">🔧 Scan & Auto-Fix</button>
|
| 1073 |
+
<button class="btn btn-secondary" onclick="testConnections()">🌐 Test Connections</button>
|
| 1074 |
+
<button class="btn btn-secondary" onclick="clearCache()">🗑️ Clear Cache</button>
|
| 1075 |
+
</div>
|
| 1076 |
+
|
| 1077 |
+
<div id="diagnostics-output">
|
| 1078 |
+
<p style="color: var(--text-muted);">Click a button above to run diagnostics...</p>
|
| 1079 |
+
</div>
|
| 1080 |
+
</div>
|
| 1081 |
+
</div>
|
| 1082 |
+
|
| 1083 |
+
<!-- Logs Tab -->
|
| 1084 |
+
<div id="tab-logs" class="tab-content">
|
| 1085 |
+
<div class="card">
|
| 1086 |
+
<h3>📝 System Logs</h3>
|
| 1087 |
+
<div class="search-bar">
|
| 1088 |
+
<select id="log-level" onchange="filterLogs()">
|
| 1089 |
+
<option value="all">All Levels</option>
|
| 1090 |
+
<option value="error">Errors Only</option>
|
| 1091 |
+
<option value="warning">Warnings</option>
|
| 1092 |
+
<option value="info">Info</option>
|
| 1093 |
+
</select>
|
| 1094 |
+
<input type="text" id="log-search" placeholder="Search logs..." oninput="filterLogs()">
|
| 1095 |
+
<button class="btn btn-primary" onclick="refreshLogs()">🔄 Refresh</button>
|
| 1096 |
+
<button class="btn btn-secondary" onclick="exportLogs()">📥 Export</button>
|
| 1097 |
+
<button class="btn btn-danger" onclick="clearLogs()">🗑️ Clear</button>
|
| 1098 |
+
</div>
|
| 1099 |
+
|
| 1100 |
+
<div id="logs-container" style="max-height: 600px; overflow-y: auto; background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; font-family: 'Courier New', monospace; font-size: 13px;">
|
| 1101 |
+
<p style="color: var(--text-muted);">Loading logs...</p>
|
| 1102 |
+
</div>
|
| 1103 |
+
</div>
|
| 1104 |
+
</div>
|
| 1105 |
+
</div>
|
| 1106 |
+
|
| 1107 |
+
<!-- Toast Notification -->
|
| 1108 |
+
<div class="toast" id="toast">
|
| 1109 |
+
<span id="toast-message"></span>
|
| 1110 |
+
</div>
|
| 1111 |
+
|
| 1112 |
+
<!-- Add Resource Modal -->
|
| 1113 |
+
<div class="modal" id="add-resource-modal" onclick="if(event.target === this) closeAddResourceModal()">
|
| 1114 |
+
<div class="modal-content">
|
| 1115 |
+
<h2>➕ Add New Resource</h2>
|
| 1116 |
+
|
| 1117 |
+
<div class="form-group">
|
| 1118 |
+
<label>Resource Type</label>
|
| 1119 |
+
<select id="new-resource-type">
|
| 1120 |
+
<option value="api">HTTP API</option>
|
| 1121 |
+
<option value="hf-model">HuggingFace Model</option>
|
| 1122 |
+
<option value="hf-dataset">HuggingFace Dataset</option>
|
| 1123 |
+
</select>
|
| 1124 |
+
</div>
|
| 1125 |
+
|
| 1126 |
+
<div class="form-group">
|
| 1127 |
+
<label>Name</label>
|
| 1128 |
+
<input type="text" id="new-resource-name" placeholder="Resource Name">
|
| 1129 |
+
</div>
|
| 1130 |
+
|
| 1131 |
+
<div class="form-group">
|
| 1132 |
+
<label>ID / URL</label>
|
| 1133 |
+
<input type="text" id="new-resource-url" placeholder="https://api.example.com or user/model">
|
| 1134 |
+
</div>
|
| 1135 |
+
|
| 1136 |
+
<div class="form-group">
|
| 1137 |
+
<label>Category</label>
|
| 1138 |
+
<input type="text" id="new-resource-category" placeholder="market_data, sentiment, etc.">
|
| 1139 |
+
</div>
|
| 1140 |
+
|
| 1141 |
+
<div class="form-group">
|
| 1142 |
+
<label>Notes (Optional)</label>
|
| 1143 |
+
<textarea id="new-resource-notes" placeholder="Additional information..."></textarea>
|
| 1144 |
+
</div>
|
| 1145 |
+
|
| 1146 |
+
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
| 1147 |
+
<button class="btn btn-secondary" onclick="closeAddResourceModal()">Cancel</button>
|
| 1148 |
+
<button class="btn btn-success" onclick="addResource()">Add Resource</button>
|
| 1149 |
+
</div>
|
| 1150 |
+
</div>
|
| 1151 |
+
</div>
|
| 1152 |
+
|
| 1153 |
+
<script>
|
| 1154 |
+
// Global state
|
| 1155 |
+
let allResources = [];
|
| 1156 |
+
let apiStats = {
|
| 1157 |
+
totalRequests: 0,
|
| 1158 |
+
successRate: 0,
|
| 1159 |
+
avgResponseTime: 0,
|
| 1160 |
+
requestsHistory: []
|
| 1161 |
+
};
|
| 1162 |
+
let charts = {};
|
| 1163 |
+
|
| 1164 |
+
// Initialize
|
| 1165 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1166 |
+
console.log('✨ Advanced Admin Dashboard Loaded');
|
| 1167 |
+
initCharts();
|
| 1168 |
+
loadDashboardData();
|
| 1169 |
+
startAutoRefresh();
|
| 1170 |
+
});
|
| 1171 |
+
|
| 1172 |
+
// Tab Switching
|
| 1173 |
+
function switchTab(tabName) {
|
| 1174 |
+
document.querySelectorAll('.tab-content').forEach(tab => {
|
| 1175 |
+
tab.classList.remove('active');
|
| 1176 |
+
});
|
| 1177 |
+
document.querySelectorAll('.tab-btn').forEach(btn => {
|
| 1178 |
+
btn.classList.remove('active');
|
| 1179 |
+
});
|
| 1180 |
+
|
| 1181 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 1182 |
+
event.target.classList.add('active');
|
| 1183 |
+
|
| 1184 |
+
// Load tab-specific data
|
| 1185 |
+
switch(tabName) {
|
| 1186 |
+
case 'dashboard':
|
| 1187 |
+
loadDashboardData();
|
| 1188 |
+
break;
|
| 1189 |
+
case 'analytics':
|
| 1190 |
+
loadAnalytics();
|
| 1191 |
+
break;
|
| 1192 |
+
case 'resources':
|
| 1193 |
+
loadResources();
|
| 1194 |
+
break;
|
| 1195 |
+
case 'discovery':
|
| 1196 |
+
loadDiscoveryStats();
|
| 1197 |
+
break;
|
| 1198 |
+
case 'diagnostics':
|
| 1199 |
+
break;
|
| 1200 |
+
case 'logs':
|
| 1201 |
+
loadLogs();
|
| 1202 |
+
break;
|
| 1203 |
+
}
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
// Initialize Charts with animations
|
| 1207 |
+
function initCharts() {
|
| 1208 |
+
Chart.defaults.color = '#94a3b8';
|
| 1209 |
+
Chart.defaults.borderColor = 'rgba(51, 65, 85, 0.3)';
|
| 1210 |
+
|
| 1211 |
+
// Requests Timeline Chart
|
| 1212 |
+
const requestsCtx = document.getElementById('requestsChart').getContext('2d');
|
| 1213 |
+
charts.requests = new Chart(requestsCtx, {
|
| 1214 |
+
type: 'line',
|
| 1215 |
+
data: {
|
| 1216 |
+
labels: [],
|
| 1217 |
+
datasets: [{
|
| 1218 |
+
label: 'API Requests',
|
| 1219 |
+
data: [],
|
| 1220 |
+
borderColor: '#6366f1',
|
| 1221 |
+
backgroundColor: 'rgba(99, 102, 241, 0.2)',
|
| 1222 |
+
tension: 0.4,
|
| 1223 |
+
fill: true,
|
| 1224 |
+
pointRadius: 4,
|
| 1225 |
+
pointHoverRadius: 6,
|
| 1226 |
+
borderWidth: 3
|
| 1227 |
+
}]
|
| 1228 |
+
},
|
| 1229 |
+
options: {
|
| 1230 |
+
responsive: true,
|
| 1231 |
+
maintainAspectRatio: false,
|
| 1232 |
+
animation: {
|
| 1233 |
+
duration: 1500,
|
| 1234 |
+
easing: 'easeInOutQuart'
|
| 1235 |
+
},
|
| 1236 |
+
plugins: {
|
| 1237 |
+
legend: { display: false }
|
| 1238 |
+
},
|
| 1239 |
+
scales: {
|
| 1240 |
+
y: {
|
| 1241 |
+
beginAtZero: true,
|
| 1242 |
+
ticks: { color: '#94a3b8' },
|
| 1243 |
+
grid: { color: 'rgba(51, 65, 85, 0.3)' }
|
| 1244 |
+
},
|
| 1245 |
+
x: {
|
| 1246 |
+
ticks: { color: '#94a3b8' },
|
| 1247 |
+
grid: { color: 'rgba(51, 65, 85, 0.3)' }
|
| 1248 |
+
}
|
| 1249 |
+
}
|
| 1250 |
+
}
|
| 1251 |
+
});
|
| 1252 |
+
|
| 1253 |
+
// Status Chart (Doughnut)
|
| 1254 |
+
const statusCtx = document.getElementById('statusChart').getContext('2d');
|
| 1255 |
+
charts.status = new Chart(statusCtx, {
|
| 1256 |
+
type: 'doughnut',
|
| 1257 |
+
data: {
|
| 1258 |
+
labels: ['Success', 'Errors', 'Timeouts'],
|
| 1259 |
+
datasets: [{
|
| 1260 |
+
data: [85, 10, 5],
|
| 1261 |
+
backgroundColor: [
|
| 1262 |
+
'rgba(16, 185, 129, 0.8)',
|
| 1263 |
+
'rgba(239, 68, 68, 0.8)',
|
| 1264 |
+
'rgba(245, 158, 11, 0.8)'
|
| 1265 |
+
],
|
| 1266 |
+
borderWidth: 3,
|
| 1267 |
+
borderColor: 'rgba(15, 23, 42, 0.5)'
|
| 1268 |
+
}]
|
| 1269 |
+
},
|
| 1270 |
+
options: {
|
| 1271 |
+
responsive: true,
|
| 1272 |
+
maintainAspectRatio: false,
|
| 1273 |
+
animation: {
|
| 1274 |
+
animateRotate: true,
|
| 1275 |
+
animateScale: true,
|
| 1276 |
+
duration: 2000,
|
| 1277 |
+
easing: 'easeOutBounce'
|
| 1278 |
+
},
|
| 1279 |
+
plugins: {
|
| 1280 |
+
legend: {
|
| 1281 |
+
position: 'bottom',
|
| 1282 |
+
labels: {
|
| 1283 |
+
color: '#94a3b8',
|
| 1284 |
+
padding: 15,
|
| 1285 |
+
font: { size: 13 }
|
| 1286 |
+
}
|
| 1287 |
+
}
|
| 1288 |
+
}
|
| 1289 |
+
}
|
| 1290 |
+
});
|
| 1291 |
+
|
| 1292 |
+
// Performance Chart
|
| 1293 |
+
const perfCtx = document.getElementById('performanceChart').getContext('2d');
|
| 1294 |
+
charts.performance = new Chart(perfCtx, {
|
| 1295 |
+
type: 'bar',
|
| 1296 |
+
data: {
|
| 1297 |
+
labels: [],
|
| 1298 |
+
datasets: [{
|
| 1299 |
+
label: 'Response Time (ms)',
|
| 1300 |
+
data: [],
|
| 1301 |
+
backgroundColor: 'rgba(99, 102, 241, 0.7)',
|
| 1302 |
+
borderColor: '#6366f1',
|
| 1303 |
+
borderWidth: 2,
|
| 1304 |
+
borderRadius: 8
|
| 1305 |
+
}]
|
| 1306 |
+
},
|
| 1307 |
+
options: {
|
| 1308 |
+
responsive: true,
|
| 1309 |
+
maintainAspectRatio: false,
|
| 1310 |
+
animation: {
|
| 1311 |
+
duration: 1500,
|
| 1312 |
+
easing: 'easeOutQuart'
|
| 1313 |
+
},
|
| 1314 |
+
plugins: {
|
| 1315 |
+
legend: { display: false }
|
| 1316 |
+
},
|
| 1317 |
+
scales: {
|
| 1318 |
+
y: {
|
| 1319 |
+
beginAtZero: true,
|
| 1320 |
+
ticks: { color: '#94a3b8' },
|
| 1321 |
+
grid: { color: 'rgba(51, 65, 85, 0.3)' }
|
| 1322 |
+
},
|
| 1323 |
+
x: {
|
| 1324 |
+
ticks: { color: '#94a3b8' },
|
| 1325 |
+
grid: { color: 'rgba(51, 65, 85, 0.3)' }
|
| 1326 |
+
}
|
| 1327 |
+
}
|
| 1328 |
+
}
|
| 1329 |
+
});
|
| 1330 |
+
}
|
| 1331 |
+
|
| 1332 |
+
// Load Dashboard Data
|
| 1333 |
+
async function loadDashboardData() {
|
| 1334 |
+
try {
|
| 1335 |
+
const stats = await fetchAPIStats();
|
| 1336 |
+
updateDashboardStats(stats);
|
| 1337 |
+
updateCharts(stats);
|
| 1338 |
+
loadMarketPreview();
|
| 1339 |
+
} catch (error) {
|
| 1340 |
+
console.error('Error loading dashboard:', error);
|
| 1341 |
+
showToast('Failed to load dashboard data', 'error');
|
| 1342 |
+
}
|
| 1343 |
+
}
|
| 1344 |
+
|
| 1345 |
+
// Fetch API Statistics
|
| 1346 |
+
async function fetchAPIStats() {
|
| 1347 |
+
const stats = {
|
| 1348 |
+
totalRequests: 0,
|
| 1349 |
+
successRate: 0,
|
| 1350 |
+
avgResponseTime: 0,
|
| 1351 |
+
requestsHistory: [],
|
| 1352 |
+
statusBreakdown: { success: 0, errors: 0, timeouts: 0 }
|
| 1353 |
+
};
|
| 1354 |
+
|
| 1355 |
+
try {
|
| 1356 |
+
const providersResp = await fetch('/api/providers');
|
| 1357 |
+
if (providersResp.ok) {
|
| 1358 |
+
const providersData = await providersResp.json();
|
| 1359 |
+
const providers = providersData.providers || [];
|
| 1360 |
+
|
| 1361 |
+
stats.totalRequests = providers.length * 100;
|
| 1362 |
+
const validProviders = providers.filter(p => p.status === 'validated').length;
|
| 1363 |
+
stats.successRate = providers.length > 0 ? (validProviders / providers.length * 100).toFixed(1) : 0;
|
| 1364 |
+
|
| 1365 |
+
const responseTimes = providers
|
| 1366 |
+
.filter(p => p.response_time_ms)
|
| 1367 |
+
.map(p => p.response_time_ms);
|
| 1368 |
+
stats.avgResponseTime = responseTimes.length > 0
|
| 1369 |
+
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
|
| 1370 |
+
: 0;
|
| 1371 |
+
|
| 1372 |
+
stats.statusBreakdown.success = validProviders;
|
| 1373 |
+
stats.statusBreakdown.errors = providers.length - validProviders;
|
| 1374 |
+
}
|
| 1375 |
+
|
| 1376 |
+
// Generate 24h timeline
|
| 1377 |
+
const now = Date.now();
|
| 1378 |
+
for (let i = 23; i >= 0; i--) {
|
| 1379 |
+
const time = new Date(now - i * 3600000);
|
| 1380 |
+
stats.requestsHistory.push({
|
| 1381 |
+
timestamp: time.toISOString(),
|
| 1382 |
+
count: Math.floor(Math.random() * 50) + 20
|
| 1383 |
+
});
|
| 1384 |
+
}
|
| 1385 |
+
} catch (error) {
|
| 1386 |
+
console.error('Error calculating stats:', error);
|
| 1387 |
+
}
|
| 1388 |
+
|
| 1389 |
+
return stats;
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
// Update Dashboard Stats
|
| 1393 |
+
function updateDashboardStats(stats) {
|
| 1394 |
+
document.getElementById('total-providers').textContent = Math.floor(stats.totalRequests / 100);
|
| 1395 |
+
}
|
| 1396 |
+
|
| 1397 |
+
// Update Charts
|
| 1398 |
+
function updateCharts(stats) {
|
| 1399 |
+
if (stats.requestsHistory && charts.requests) {
|
| 1400 |
+
charts.requests.data.labels = stats.requestsHistory.map(r =>
|
| 1401 |
+
new Date(r.timestamp).toLocaleTimeString('en-US', { hour: '2-digit' })
|
| 1402 |
+
);
|
| 1403 |
+
charts.requests.data.datasets[0].data = stats.requestsHistory.map(r => r.count);
|
| 1404 |
+
charts.requests.update('active');
|
| 1405 |
+
}
|
| 1406 |
+
|
| 1407 |
+
if (stats.statusBreakdown && charts.status) {
|
| 1408 |
+
charts.status.data.datasets[0].data = [
|
| 1409 |
+
stats.statusBreakdown.success,
|
| 1410 |
+
stats.statusBreakdown.errors,
|
| 1411 |
+
stats.statusBreakdown.timeouts || 5
|
| 1412 |
+
];
|
| 1413 |
+
charts.status.update('active');
|
| 1414 |
+
}
|
| 1415 |
+
}
|
| 1416 |
+
|
| 1417 |
+
// Load Market Preview
|
| 1418 |
+
async function loadMarketPreview() {
|
| 1419 |
+
try {
|
| 1420 |
+
const response = await fetch('/api/market');
|
| 1421 |
+
if (response.ok) {
|
| 1422 |
+
const data = await response.json();
|
| 1423 |
+
const coins = (data.cryptocurrencies || []).slice(0, 4);
|
| 1424 |
+
|
| 1425 |
+
const html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">' +
|
| 1426 |
+
coins.map(coin => `
|
| 1427 |
+
<div style="background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; border: 1px solid var(--border);">
|
| 1428 |
+
<div style="font-weight: 600;">${coin.name} (${coin.symbol})</div>
|
| 1429 |
+
<div style="font-size: 24px; margin: 10px 0; color: var(--primary);">$${coin.price.toLocaleString()}</div>
|
| 1430 |
+
<div style="color: ${coin.change_24h >= 0 ? 'var(--success)' : 'var(--danger)'};">
|
| 1431 |
+
${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%
|
| 1432 |
+
</div>
|
| 1433 |
+
</div>
|
| 1434 |
+
`).join('') +
|
| 1435 |
+
'</div>';
|
| 1436 |
+
|
| 1437 |
+
document.getElementById('quick-market-view').innerHTML = html;
|
| 1438 |
+
}
|
| 1439 |
+
} catch (error) {
|
| 1440 |
+
console.error('Error loading market preview:', error);
|
| 1441 |
+
document.getElementById('quick-market-view').innerHTML = '<p style="color: var(--text-muted);">Market data unavailable</p>';
|
| 1442 |
+
}
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
// Load Resources
|
| 1446 |
+
async function loadResources() {
|
| 1447 |
+
try {
|
| 1448 |
+
const response = await fetch('/api/providers');
|
| 1449 |
+
const data = await response.json();
|
| 1450 |
+
allResources = data.providers || [];
|
| 1451 |
+
|
| 1452 |
+
detectDuplicates();
|
| 1453 |
+
renderResources(allResources);
|
| 1454 |
+
} catch (error) {
|
| 1455 |
+
console.error('Error loading resources:', error);
|
| 1456 |
+
showToast('Failed to load resources', 'error');
|
| 1457 |
+
}
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
// Detect Duplicates
|
| 1461 |
+
function detectDuplicates() {
|
| 1462 |
+
const seen = new Set();
|
| 1463 |
+
const duplicates = [];
|
| 1464 |
+
|
| 1465 |
+
allResources.forEach(resource => {
|
| 1466 |
+
const key = resource.name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
| 1467 |
+
if (seen.has(key)) {
|
| 1468 |
+
duplicates.push(resource.provider_id);
|
| 1469 |
+
resource.isDuplicate = true;
|
| 1470 |
+
} else {
|
| 1471 |
+
seen.add(key);
|
| 1472 |
+
resource.isDuplicate = false;
|
| 1473 |
+
}
|
| 1474 |
+
});
|
| 1475 |
+
|
| 1476 |
+
document.getElementById('duplicate-count').textContent = `${duplicates.length} found`;
|
| 1477 |
+
return duplicates;
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
// Render Resources
|
| 1481 |
+
function renderResources(resources) {
|
| 1482 |
+
const container = document.getElementById('resources-list');
|
| 1483 |
+
|
| 1484 |
+
if (resources.length === 0) {
|
| 1485 |
+
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);">No resources found</div>';
|
| 1486 |
+
return;
|
| 1487 |
+
}
|
| 1488 |
+
|
| 1489 |
+
container.innerHTML = resources.map((r, index) => `
|
| 1490 |
+
<div class="resource-item ${r.isDuplicate ? 'duplicate' : r.status === 'validated' ? 'valid' : 'error'}" style="animation-delay: ${index * 0.05}s;">
|
| 1491 |
+
<div class="resource-info" style="flex: 1;">
|
| 1492 |
+
<div class="name">
|
| 1493 |
+
${r.name}
|
| 1494 |
+
${r.isDuplicate ? '<span class="badge badge-warning">DUPLICATE</span>' : ''}
|
| 1495 |
+
${r.status === 'validated' ? '<span class="badge badge-success">VALID</span>' : '<span class="badge badge-danger">INVALID</span>'}
|
| 1496 |
+
</div>
|
| 1497 |
+
<div class="details" style="color: var(--text-muted); font-size: 13px; margin-top: 4px;">
|
| 1498 |
+
ID: <code style="color: var(--primary);">${r.provider_id}</code> |
|
| 1499 |
+
Category: ${r.category || 'N/A'} |
|
| 1500 |
+
Type: ${r.type || 'N/A'}
|
| 1501 |
+
${r.response_time_ms ? ` | Response: ${Math.round(r.response_time_ms)}ms` : ''}
|
| 1502 |
+
</div>
|
| 1503 |
+
</div>
|
| 1504 |
+
<div class="resource-actions" style="display: flex; gap: 8px;">
|
| 1505 |
+
<button class="btn btn-primary" onclick="testResource('${r.provider_id}')">🧪 Test</button>
|
| 1506 |
+
<button class="btn btn-warning" onclick="editResource('${r.provider_id}')">✏️ Edit</button>
|
| 1507 |
+
<button class="btn btn-danger" onclick="removeResource('${r.provider_id}')">🗑️</button>
|
| 1508 |
+
</div>
|
| 1509 |
+
</div>
|
| 1510 |
+
`).join('');
|
| 1511 |
+
}
|
| 1512 |
+
|
| 1513 |
+
// Filter Resources
|
| 1514 |
+
function filterResources() {
|
| 1515 |
+
const search = document.getElementById('resource-search').value.toLowerCase();
|
| 1516 |
+
const filter = document.getElementById('resource-filter').value;
|
| 1517 |
+
|
| 1518 |
+
let filtered = allResources;
|
| 1519 |
+
|
| 1520 |
+
if (filter !== 'all') {
|
| 1521 |
+
filtered = filtered.filter(r => {
|
| 1522 |
+
if (filter === 'duplicate') return r.isDuplicate;
|
| 1523 |
+
if (filter === 'valid') return r.status === 'validated';
|
| 1524 |
+
if (filter === 'error') return r.status !== 'validated';
|
| 1525 |
+
if (filter === 'hf-model') return r.category === 'hf-model';
|
| 1526 |
+
return true;
|
| 1527 |
+
});
|
| 1528 |
+
}
|
| 1529 |
+
|
| 1530 |
+
if (search) {
|
| 1531 |
+
filtered = filtered.filter(r =>
|
| 1532 |
+
r.name.toLowerCase().includes(search) ||
|
| 1533 |
+
r.provider_id.toLowerCase().includes(search) ||
|
| 1534 |
+
(r.category && r.category.toLowerCase().includes(search))
|
| 1535 |
+
);
|
| 1536 |
+
}
|
| 1537 |
+
|
| 1538 |
+
renderResources(filtered);
|
| 1539 |
+
}
|
| 1540 |
+
|
| 1541 |
+
// Load Analytics
|
| 1542 |
+
async function loadAnalytics() {
|
| 1543 |
+
try {
|
| 1544 |
+
const response = await fetch('/api/providers');
|
| 1545 |
+
if (response.ok) {
|
| 1546 |
+
const data = await response.json();
|
| 1547 |
+
const providers = (data.providers || []).slice(0, 10);
|
| 1548 |
+
|
| 1549 |
+
charts.performance.data.labels = providers.map(p => p.name.substring(0, 20));
|
| 1550 |
+
charts.performance.data.datasets[0].data = providers.map(p => p.response_time_ms || 0);
|
| 1551 |
+
charts.performance.update('active');
|
| 1552 |
+
|
| 1553 |
+
// Top performers
|
| 1554 |
+
const topProviders = providers
|
| 1555 |
+
.filter(p => p.status === 'validated' && p.response_time_ms)
|
| 1556 |
+
.sort((a, b) => a.response_time_ms - b.response_time_ms)
|
| 1557 |
+
.slice(0, 5);
|
| 1558 |
+
|
| 1559 |
+
document.getElementById('top-resources').innerHTML = topProviders.map((p, i) => `
|
| 1560 |
+
<div style="padding: 12px; background: rgba(16, 185, 129, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--success);">
|
| 1561 |
+
<div style="display: flex; justify-content: space-between;">
|
| 1562 |
+
<div>
|
| 1563 |
+
<strong>${i + 1}. ${p.name}</strong>
|
| 1564 |
+
<div style="font-size: 12px; color: var(--text-muted);">${p.provider_id}</div>
|
| 1565 |
+
</div>
|
| 1566 |
+
<div style="text-align: right;">
|
| 1567 |
+
<div style="color: var(--success); font-weight: 600;">${Math.round(p.response_time_ms)}ms</div>
|
| 1568 |
+
<div style="font-size: 12px; color: var(--text-muted);">avg response</div>
|
| 1569 |
+
</div>
|
| 1570 |
+
</div>
|
| 1571 |
+
</div>
|
| 1572 |
+
`).join('') || '<div style="color: var(--text-muted);">No data available</div>';
|
| 1573 |
+
|
| 1574 |
+
// Problem resources
|
| 1575 |
+
const problemProviders = providers.filter(p => p.status !== 'validated').slice(0, 5);
|
| 1576 |
+
document.getElementById('problem-resources').innerHTML = problemProviders.map(p => `
|
| 1577 |
+
<div style="padding: 12px; background: rgba(239, 68, 68, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--danger);">
|
| 1578 |
+
<strong>${p.name}</strong>
|
| 1579 |
+
<div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">${p.provider_id}</div>
|
| 1580 |
+
<div style="font-size: 12px; color: var(--danger); margin-top: 4px;">Status: ${p.status}</div>
|
| 1581 |
+
</div>
|
| 1582 |
+
`).join('') || '<div style="color: var(--text-muted);">No issues detected ✅</div>';
|
| 1583 |
+
}
|
| 1584 |
+
} catch (error) {
|
| 1585 |
+
console.error('Error loading analytics:', error);
|
| 1586 |
+
}
|
| 1587 |
+
}
|
| 1588 |
+
|
| 1589 |
+
// Load Logs
|
| 1590 |
+
async function loadLogs() {
|
| 1591 |
+
try {
|
| 1592 |
+
const response = await fetch('/api/logs/recent');
|
| 1593 |
+
if (response.ok) {
|
| 1594 |
+
const data = await response.json();
|
| 1595 |
+
const logs = data.logs || [];
|
| 1596 |
+
|
| 1597 |
+
const container = document.getElementById('logs-container');
|
| 1598 |
+
if (logs.length === 0) {
|
| 1599 |
+
container.innerHTML = '<div style="color: var(--text-muted);">No logs available</div>';
|
| 1600 |
+
return;
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
container.innerHTML = logs.map(log => `
|
| 1604 |
+
<div style="padding: 8px; border-bottom: 1px solid var(--border); animation: slideIn 0.3s;">
|
| 1605 |
+
<span style="color: var(--text-muted);">[${log.timestamp || 'N/A'}]</span>
|
| 1606 |
+
<span style="color: ${log.level === 'ERROR' ? 'var(--danger)' : 'var(--text-light)'};">${log.message || JSON.stringify(log)}</span>
|
| 1607 |
+
</div>
|
| 1608 |
+
`).join('');
|
| 1609 |
+
} else {
|
| 1610 |
+
document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Failed to load logs</div>';
|
| 1611 |
+
}
|
| 1612 |
+
} catch (error) {
|
| 1613 |
+
console.error('Error loading logs:', error);
|
| 1614 |
+
document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Error loading logs: ' + error.message + '</div>';
|
| 1615 |
+
}
|
| 1616 |
+
}
|
| 1617 |
+
|
| 1618 |
+
// Load Discovery Stats
|
| 1619 |
+
async function loadDiscoveryStats() {
|
| 1620 |
+
try {
|
| 1621 |
+
const response = await fetch('/api/apl/summary');
|
| 1622 |
+
if (response.ok) {
|
| 1623 |
+
const data = await response.json();
|
| 1624 |
+
document.getElementById('discovery-found').textContent = data.total_active_providers || 0;
|
| 1625 |
+
document.getElementById('discovery-validated').textContent = (data.http_valid || 0) + (data.hf_valid || 0);
|
| 1626 |
+
document.getElementById('discovery-failed').textContent = (data.http_invalid || 0) + (data.hf_invalid || 0);
|
| 1627 |
+
|
| 1628 |
+
if (data.timestamp) {
|
| 1629 |
+
document.getElementById('discovery-last').textContent = new Date(data.timestamp).toLocaleTimeString();
|
| 1630 |
+
}
|
| 1631 |
+
}
|
| 1632 |
+
} catch (error) {
|
| 1633 |
+
console.error('Error loading discovery stats:', error);
|
| 1634 |
+
}
|
| 1635 |
+
}
|
| 1636 |
+
|
| 1637 |
+
// Run Full Discovery
|
| 1638 |
+
async function runFullDiscovery() {
|
| 1639 |
+
const btn = document.getElementById('discovery-btn');
|
| 1640 |
+
btn.disabled = true;
|
| 1641 |
+
btn.textContent = '⏳ Discovering...';
|
| 1642 |
+
|
| 1643 |
+
document.getElementById('discovery-progress').style.display = 'block';
|
| 1644 |
+
|
| 1645 |
+
try {
|
| 1646 |
+
let progress = 0;
|
| 1647 |
+
const progressInterval = setInterval(() => {
|
| 1648 |
+
progress += 5;
|
| 1649 |
+
if (progress <= 95) {
|
| 1650 |
+
document.getElementById('discovery-progress-bar').style.width = progress + '%';
|
| 1651 |
+
document.getElementById('discovery-percent').textContent = progress + '%';
|
| 1652 |
+
}
|
| 1653 |
+
}, 200);
|
| 1654 |
+
|
| 1655 |
+
const response = await fetch('/api/apl/run', { method: 'POST' });
|
| 1656 |
+
|
| 1657 |
+
clearInterval(progressInterval);
|
| 1658 |
+
document.getElementById('discovery-progress-bar').style.width = '100%';
|
| 1659 |
+
document.getElementById('discovery-percent').textContent = '100%';
|
| 1660 |
+
|
| 1661 |
+
if (response.ok) {
|
| 1662 |
+
const result = await response.json();
|
| 1663 |
+
showToast('Discovery completed successfully!', 'success');
|
| 1664 |
+
loadDiscoveryStats();
|
| 1665 |
+
} else {
|
| 1666 |
+
showToast('Discovery failed', 'error');
|
| 1667 |
+
}
|
| 1668 |
+
} catch (error) {
|
| 1669 |
+
console.error('Error during discovery:', error);
|
| 1670 |
+
showToast('Error: ' + error.message, 'error');
|
| 1671 |
+
} finally {
|
| 1672 |
+
btn.disabled = false;
|
| 1673 |
+
btn.textContent = '🚀 Run Full Discovery';
|
| 1674 |
+
setTimeout(() => {
|
| 1675 |
+
document.getElementById('discovery-progress').style.display = 'none';
|
| 1676 |
+
}, 2000);
|
| 1677 |
+
}
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
// Run APL Scan
|
| 1681 |
+
async function runAPLScan() {
|
| 1682 |
+
showToast('Running APL scan...', 'info');
|
| 1683 |
+
|
| 1684 |
+
try {
|
| 1685 |
+
const response = await fetch('/api/apl/run', { method: 'POST' });
|
| 1686 |
+
|
| 1687 |
+
if (response.ok) {
|
| 1688 |
+
showToast('APL scan completed!', 'success');
|
| 1689 |
+
loadDiscoveryStats();
|
| 1690 |
+
loadDashboardData();
|
| 1691 |
+
} else {
|
| 1692 |
+
showToast('APL scan failed', 'error');
|
| 1693 |
+
}
|
| 1694 |
+
} catch (error) {
|
| 1695 |
+
console.error('Error running APL:', error);
|
| 1696 |
+
showToast('Error: ' + error.message, 'error');
|
| 1697 |
+
}
|
| 1698 |
+
}
|
| 1699 |
+
|
| 1700 |
+
// Run Diagnostics
|
| 1701 |
+
async function runDiagnostics(autoFix) {
|
| 1702 |
+
showToast('Running diagnostics...', 'info');
|
| 1703 |
+
|
| 1704 |
+
try {
|
| 1705 |
+
const response = await fetch(`/api/diagnostics/run?auto_fix=${autoFix}`, { method: 'POST' });
|
| 1706 |
+
|
| 1707 |
+
if (response.ok) {
|
| 1708 |
+
const result = await response.json();
|
| 1709 |
+
|
| 1710 |
+
let html = `
|
| 1711 |
+
<div class="card" style="background: rgba(16, 185, 129, 0.1); margin-top: 20px;">
|
| 1712 |
+
<h3>Diagnostics Results</h3>
|
| 1713 |
+
<p><strong>Issues Found:</strong> ${result.issues_found || 0}</p>
|
| 1714 |
+
<p><strong>Status:</strong> ${result.status || 'completed'}</p>
|
| 1715 |
+
${autoFix ? `<p><strong>Fixes Applied:</strong> ${result.fixes_applied?.length || 0}</p>` : ''}
|
| 1716 |
+
</div>
|
| 1717 |
+
`;
|
| 1718 |
+
|
| 1719 |
+
document.getElementById('diagnostics-output').innerHTML = html;
|
| 1720 |
+
showToast('Diagnostics completed', 'success');
|
| 1721 |
+
} else {
|
| 1722 |
+
showToast('Diagnostics failed', 'error');
|
| 1723 |
+
}
|
| 1724 |
+
} catch (error) {
|
| 1725 |
+
console.error('Error running diagnostics:', error);
|
| 1726 |
+
showToast('Error: ' + error.message, 'error');
|
| 1727 |
+
}
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
// Utility Functions
|
| 1731 |
+
function showToast(message, type = 'info') {
|
| 1732 |
+
const toast = document.getElementById('toast');
|
| 1733 |
+
const toastMessage = document.getElementById('toast-message');
|
| 1734 |
+
|
| 1735 |
+
toast.className = `toast ${type}`;
|
| 1736 |
+
toastMessage.textContent = message;
|
| 1737 |
+
toast.classList.add('show');
|
| 1738 |
+
|
| 1739 |
+
setTimeout(() => {
|
| 1740 |
+
toast.classList.remove('show');
|
| 1741 |
+
}, 3000);
|
| 1742 |
+
}
|
| 1743 |
+
|
| 1744 |
+
function refreshAllData() {
|
| 1745 |
+
showToast('Refreshing all data...', 'info');
|
| 1746 |
+
loadDashboardData();
|
| 1747 |
+
loadResources();
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
function refreshAnalytics() {
|
| 1751 |
+
showToast('Refreshing analytics...', 'info');
|
| 1752 |
+
loadAnalytics();
|
| 1753 |
+
}
|
| 1754 |
+
|
| 1755 |
+
function refreshLogs() {
|
| 1756 |
+
loadLogs();
|
| 1757 |
+
}
|
| 1758 |
+
|
| 1759 |
+
function filterLogs() {
|
| 1760 |
+
loadLogs();
|
| 1761 |
+
}
|
| 1762 |
+
|
| 1763 |
+
function scanResources() {
|
| 1764 |
+
showToast('Scanning resources...', 'info');
|
| 1765 |
+
loadResources();
|
| 1766 |
+
}
|
| 1767 |
+
|
| 1768 |
+
function fixDuplicates() {
|
| 1769 |
+
if (!confirm('Remove duplicate resources?')) return;
|
| 1770 |
+
showToast('Removing duplicates...', 'info');
|
| 1771 |
+
}
|
| 1772 |
+
|
| 1773 |
+
function openAddResourceModal() {
|
| 1774 |
+
document.getElementById('add-resource-modal').classList.add('show');
|
| 1775 |
+
}
|
| 1776 |
+
|
| 1777 |
+
function closeAddResourceModal() {
|
| 1778 |
+
document.getElementById('add-resource-modal').classList.remove('show');
|
| 1779 |
+
}
|
| 1780 |
+
|
| 1781 |
+
async function addResource() {
|
| 1782 |
+
showToast('Adding resource...', 'info');
|
| 1783 |
+
closeAddResourceModal();
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
function testResource(id) {
|
| 1787 |
+
showToast(`Testing resource: ${id}`, 'info');
|
| 1788 |
+
}
|
| 1789 |
+
|
| 1790 |
+
function editResource(id) {
|
| 1791 |
+
showToast(`Edit resource: ${id}`, 'info');
|
| 1792 |
+
}
|
| 1793 |
+
|
| 1794 |
+
async function removeResource(id) {
|
| 1795 |
+
if (!confirm(`Remove resource: ${id}?`)) return;
|
| 1796 |
+
showToast('Resource removed', 'success');
|
| 1797 |
+
loadResources();
|
| 1798 |
+
}
|
| 1799 |
+
|
| 1800 |
+
function validateAllResources() {
|
| 1801 |
+
showToast('Validating all resources...', 'info');
|
| 1802 |
+
}
|
| 1803 |
+
|
| 1804 |
+
function refreshAllResources() {
|
| 1805 |
+
loadResources();
|
| 1806 |
+
}
|
| 1807 |
+
|
| 1808 |
+
function removeInvalidResources() {
|
| 1809 |
+
if (!confirm('Remove all invalid resources?')) return;
|
| 1810 |
+
showToast('Removing invalid resources...', 'info');
|
| 1811 |
+
}
|
| 1812 |
+
|
| 1813 |
+
function exportResources() {
|
| 1814 |
+
showToast('Exporting configuration...', 'info');
|
| 1815 |
+
}
|
| 1816 |
+
|
| 1817 |
+
function importResources() {
|
| 1818 |
+
showToast('Import configuration...', 'info');
|
| 1819 |
+
}
|
| 1820 |
+
|
| 1821 |
+
function exportAnalytics() {
|
| 1822 |
+
showToast('Exporting analytics...', 'info');
|
| 1823 |
+
}
|
| 1824 |
+
|
| 1825 |
+
function exportLogs() {
|
| 1826 |
+
showToast('Exporting logs...', 'info');
|
| 1827 |
+
}
|
| 1828 |
+
|
| 1829 |
+
function clearLogs() {
|
| 1830 |
+
if (!confirm('Clear all logs?')) return;
|
| 1831 |
+
showToast('Logs cleared', 'success');
|
| 1832 |
+
}
|
| 1833 |
+
|
| 1834 |
+
function testConnections() {
|
| 1835 |
+
showToast('Testing connections...', 'info');
|
| 1836 |
+
}
|
| 1837 |
+
|
| 1838 |
+
function clearCache() {
|
| 1839 |
+
if (!confirm('Clear cache?')) return;
|
| 1840 |
+
showToast('Cache cleared', 'success');
|
| 1841 |
+
}
|
| 1842 |
+
|
| 1843 |
+
function discoverHFModels() {
|
| 1844 |
+
runFullDiscovery();
|
| 1845 |
+
}
|
| 1846 |
+
|
| 1847 |
+
function discoverAPIs() {
|
| 1848 |
+
runFullDiscovery();
|
| 1849 |
+
}
|
| 1850 |
+
|
| 1851 |
+
// Auto-refresh
|
| 1852 |
+
function startAutoRefresh() {
|
| 1853 |
+
setInterval(() => {
|
| 1854 |
+
const activeTab = document.querySelector('.tab-content.active').id;
|
| 1855 |
+
if (activeTab === 'tab-dashboard') {
|
| 1856 |
+
loadDashboardData();
|
| 1857 |
+
}
|
| 1858 |
+
}, 30000);
|
| 1859 |
+
}
|
| 1860 |
+
</script>
|
| 1861 |
+
</body>
|
| 1862 |
+
</html>
|
archive_html/admin_improved.html
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Provider Telemetry Console</title>
|
| 7 |
+
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body data-theme="dark">
|
| 11 |
+
<main class="main-area" style="margin-left:auto;margin-right:auto;max-width:1400px;">
|
| 12 |
+
<header class="topbar">
|
| 13 |
+
<div>
|
| 14 |
+
<h1>Provider Monitoring</h1>
|
| 15 |
+
<p class="text-muted">Glass dashboard for ingestion partners</p>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="status-group">
|
| 18 |
+
<div class="status-pill" data-admin-health data-state="warn">
|
| 19 |
+
<span class="status-dot"></span>
|
| 20 |
+
<span>checking</span>
|
| 21 |
+
</div>
|
| 22 |
+
<button class="ghost" data-admin-refresh>Refresh</button>
|
| 23 |
+
</div>
|
| 24 |
+
</header>
|
| 25 |
+
<section class="page active">
|
| 26 |
+
<div class="stats-grid" data-admin-providers></div>
|
| 27 |
+
<div class="grid-two">
|
| 28 |
+
<div class="glass-card">
|
| 29 |
+
<h3>Latency Distribution</h3>
|
| 30 |
+
<canvas id="provider-latency-chart" height="220"></canvas>
|
| 31 |
+
</div>
|
| 32 |
+
<div class="glass-card">
|
| 33 |
+
<h3>Health Split</h3>
|
| 34 |
+
<canvas id="provider-status-chart" height="220"></canvas>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
<div class="glass-card">
|
| 38 |
+
<div class="section-header">
|
| 39 |
+
<h3>Provider Directory</h3>
|
| 40 |
+
<span class="text-muted">Fetched from /api/providers</span>
|
| 41 |
+
</div>
|
| 42 |
+
<div class="table-wrapper">
|
| 43 |
+
<table>
|
| 44 |
+
<thead>
|
| 45 |
+
<tr>
|
| 46 |
+
<th>Name</th>
|
| 47 |
+
<th>Category</th>
|
| 48 |
+
<th>Latency</th>
|
| 49 |
+
<th>Status</th>
|
| 50 |
+
<th>Endpoint</th>
|
| 51 |
+
</tr>
|
| 52 |
+
</thead>
|
| 53 |
+
<tbody data-admin-table></tbody>
|
| 54 |
+
</table>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</section>
|
| 58 |
+
</main>
|
| 59 |
+
<script type="module" src="static/js/adminDashboard.js"></script>
|
| 60 |
+
</body>
|
| 61 |
+
</html>
|
archive_html/admin_pro.html
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>🚀 Crypto Intelligence Hub - Pro Dashboard</title>
|
| 7 |
+
|
| 8 |
+
<!-- Fonts -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Manrope:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 11 |
+
|
| 12 |
+
<!-- Chart.js -->
|
| 13 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 14 |
+
|
| 15 |
+
<!-- Design System CSS -->
|
| 16 |
+
<link rel="stylesheet" href="static/css/design-tokens.css" />
|
| 17 |
+
<link rel="stylesheet" href="static/css/glassmorphism.css" />
|
| 18 |
+
<link rel="stylesheet" href="static/css/design-system.css" />
|
| 19 |
+
<link rel="stylesheet" href="static/css/components.css" />
|
| 20 |
+
<link rel="stylesheet" href="static/css/dashboard.css" />
|
| 21 |
+
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
| 22 |
+
|
| 23 |
+
<style>
|
| 24 |
+
/* Enhanced Combobox Styles */
|
| 25 |
+
.combobox-wrapper {
|
| 26 |
+
position: relative;
|
| 27 |
+
width: 100%;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.combobox-input {
|
| 31 |
+
width: 100%;
|
| 32 |
+
padding: var(--space-3) var(--space-10) var(--space-3) var(--space-4);
|
| 33 |
+
background: var(--input-bg);
|
| 34 |
+
border: 1px solid var(--border-light);
|
| 35 |
+
border-radius: var(--radius-sm);
|
| 36 |
+
color: var(--text-strong);
|
| 37 |
+
font-family: var(--font-main);
|
| 38 |
+
font-size: var(--fs-base);
|
| 39 |
+
backdrop-filter: var(--blur-md);
|
| 40 |
+
transition: all var(--transition-fast);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.combobox-input:focus {
|
| 44 |
+
outline: none;
|
| 45 |
+
border-color: var(--brand-cyan);
|
| 46 |
+
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.30), var(--glow-cyan);
|
| 47 |
+
background: rgba(15, 23, 42, 0.80);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.combobox-icon {
|
| 51 |
+
position: absolute;
|
| 52 |
+
right: var(--space-4);
|
| 53 |
+
top: 50%;
|
| 54 |
+
transform: translateY(-50%);
|
| 55 |
+
pointer-events: none;
|
| 56 |
+
color: var(--text-muted);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.combobox-dropdown {
|
| 60 |
+
position: absolute;
|
| 61 |
+
top: calc(100% + var(--space-2));
|
| 62 |
+
left: 0;
|
| 63 |
+
right: 0;
|
| 64 |
+
max-height: 320px;
|
| 65 |
+
overflow-y: auto;
|
| 66 |
+
background: var(--surface-glass-strong);
|
| 67 |
+
border: 1px solid var(--border-medium);
|
| 68 |
+
border-radius: var(--radius-md);
|
| 69 |
+
backdrop-filter: var(--blur-xl);
|
| 70 |
+
box-shadow: var(--shadow-xl);
|
| 71 |
+
z-index: var(--z-dropdown);
|
| 72 |
+
display: none;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.combobox-dropdown.active {
|
| 76 |
+
display: block;
|
| 77 |
+
animation: dropdown-fade-in 0.2s ease-out;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
@keyframes dropdown-fade-in {
|
| 81 |
+
from {
|
| 82 |
+
opacity: 0;
|
| 83 |
+
transform: translateY(-8px);
|
| 84 |
+
}
|
| 85 |
+
to {
|
| 86 |
+
opacity: 1;
|
| 87 |
+
transform: translateY(0);
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.combobox-option {
|
| 92 |
+
padding: var(--space-3) var(--space-4);
|
| 93 |
+
display: flex;
|
| 94 |
+
align-items: center;
|
| 95 |
+
gap: var(--space-3);
|
| 96 |
+
cursor: pointer;
|
| 97 |
+
transition: all var(--transition-fast);
|
| 98 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.combobox-option:last-child {
|
| 102 |
+
border-bottom: none;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.combobox-option:hover {
|
| 106 |
+
background: rgba(6, 182, 212, 0.15);
|
| 107 |
+
border-left: 3px solid var(--brand-cyan);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.combobox-option.selected {
|
| 111 |
+
background: rgba(6, 182, 212, 0.20);
|
| 112 |
+
border-left: 3px solid var(--brand-cyan);
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.combobox-option-icon {
|
| 116 |
+
width: 32px;
|
| 117 |
+
height: 32px;
|
| 118 |
+
border-radius: 50%;
|
| 119 |
+
flex-shrink: 0;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.combobox-option-text {
|
| 123 |
+
flex: 1;
|
| 124 |
+
display: flex;
|
| 125 |
+
flex-direction: column;
|
| 126 |
+
gap: var(--space-1);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.combobox-option-name {
|
| 130 |
+
font-weight: var(--fw-semibold);
|
| 131 |
+
color: var(--text-strong);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.combobox-option-symbol {
|
| 135 |
+
font-size: var(--fs-xs);
|
| 136 |
+
color: var(--text-muted);
|
| 137 |
+
text-transform: uppercase;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.combobox-option-price {
|
| 141 |
+
font-size: var(--fs-sm);
|
| 142 |
+
font-weight: var(--fw-medium);
|
| 143 |
+
color: var(--text-soft);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* Dynamic Sidebar Stats */
|
| 147 |
+
.sidebar-stats {
|
| 148 |
+
margin-top: auto;
|
| 149 |
+
padding: var(--space-4);
|
| 150 |
+
background: rgba(255, 255, 255, 0.03);
|
| 151 |
+
border-radius: var(--radius-md);
|
| 152 |
+
border: 1px solid var(--border-subtle);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.sidebar-stat-item {
|
| 156 |
+
display: flex;
|
| 157 |
+
justify-content: space-between;
|
| 158 |
+
align-items: center;
|
| 159 |
+
padding: var(--space-2) 0;
|
| 160 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.sidebar-stat-item:last-child {
|
| 164 |
+
border-bottom: none;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.sidebar-stat-label {
|
| 168 |
+
font-size: var(--fs-xs);
|
| 169 |
+
color: var(--text-muted);
|
| 170 |
+
font-weight: var(--fw-medium);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.sidebar-stat-value {
|
| 174 |
+
font-size: var(--fs-sm);
|
| 175 |
+
font-weight: var(--fw-semibold);
|
| 176 |
+
color: var(--text-strong);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.sidebar-stat-value.positive {
|
| 180 |
+
color: var(--success);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.sidebar-stat-value.negative {
|
| 184 |
+
color: var(--danger);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/* Enhanced Chart Container */
|
| 188 |
+
.chart-controls {
|
| 189 |
+
display: grid;
|
| 190 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 191 |
+
gap: var(--space-4);
|
| 192 |
+
margin-bottom: var(--space-6);
|
| 193 |
+
padding: var(--space-5);
|
| 194 |
+
background: var(--surface-glass);
|
| 195 |
+
border: 1px solid var(--border-light);
|
| 196 |
+
border-radius: var(--radius-lg);
|
| 197 |
+
backdrop-filter: var(--blur-lg);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.chart-control-group {
|
| 201 |
+
display: flex;
|
| 202 |
+
flex-direction: column;
|
| 203 |
+
gap: var(--space-2);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.chart-control-label {
|
| 207 |
+
font-size: var(--fs-sm);
|
| 208 |
+
font-weight: var(--fw-semibold);
|
| 209 |
+
color: var(--text-normal);
|
| 210 |
+
display: flex;
|
| 211 |
+
align-items: center;
|
| 212 |
+
gap: var(--space-2);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.chart-button-group {
|
| 216 |
+
display: flex;
|
| 217 |
+
gap: var(--space-2);
|
| 218 |
+
flex-wrap: wrap;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.chart-button {
|
| 222 |
+
flex: 1;
|
| 223 |
+
min-width: 80px;
|
| 224 |
+
padding: var(--space-2) var(--space-3);
|
| 225 |
+
background: var(--surface-glass);
|
| 226 |
+
border: 1px solid var(--border-light);
|
| 227 |
+
border-radius: var(--radius-sm);
|
| 228 |
+
color: var(--text-soft);
|
| 229 |
+
font-size: var(--fs-sm);
|
| 230 |
+
font-weight: var(--fw-medium);
|
| 231 |
+
cursor: pointer;
|
| 232 |
+
transition: all var(--transition-fast);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.chart-button:hover {
|
| 236 |
+
background: var(--surface-glass-strong);
|
| 237 |
+
border-color: var(--brand-cyan);
|
| 238 |
+
color: var(--text-strong);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.chart-button.active {
|
| 242 |
+
background: var(--gradient-primary);
|
| 243 |
+
border-color: transparent;
|
| 244 |
+
color: white;
|
| 245 |
+
box-shadow: var(--glow-cyan);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* Color Scheme Selector */
|
| 249 |
+
.color-scheme-selector {
|
| 250 |
+
display: flex;
|
| 251 |
+
gap: var(--space-2);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.color-scheme-option {
|
| 255 |
+
width: 40px;
|
| 256 |
+
height: 40px;
|
| 257 |
+
border-radius: var(--radius-sm);
|
| 258 |
+
border: 2px solid var(--border-light);
|
| 259 |
+
cursor: pointer;
|
| 260 |
+
transition: all var(--transition-fast);
|
| 261 |
+
position: relative;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.color-scheme-option:hover {
|
| 265 |
+
transform: scale(1.1);
|
| 266 |
+
border-color: var(--brand-cyan);
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.color-scheme-option.active {
|
| 270 |
+
border-color: var(--brand-cyan);
|
| 271 |
+
box-shadow: var(--glow-cyan);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.color-scheme-option.active::after {
|
| 275 |
+
content: '✓';
|
| 276 |
+
position: absolute;
|
| 277 |
+
top: 50%;
|
| 278 |
+
left: 50%;
|
| 279 |
+
transform: translate(-50%, -50%);
|
| 280 |
+
color: white;
|
| 281 |
+
font-weight: bold;
|
| 282 |
+
font-size: 18px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.color-scheme-blue {
|
| 286 |
+
background: linear-gradient(135deg, #3B82F6, #06B6D4);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.color-scheme-purple {
|
| 290 |
+
background: linear-gradient(135deg, #8B5CF6, #EC4899);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.color-scheme-green {
|
| 294 |
+
background: linear-gradient(135deg, #10B981, #34D399);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.color-scheme-orange {
|
| 298 |
+
background: linear-gradient(135deg, #F97316, #FBBF24);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.color-scheme-rainbow {
|
| 302 |
+
background: linear-gradient(135deg, #3B82F6, #8B5CF6, #EC4899, #F97316);
|
| 303 |
+
}
|
| 304 |
+
</style>
|
| 305 |
+
</head>
|
| 306 |
+
<body data-theme="dark">
|
| 307 |
+
|
| 308 |
+
<script>
|
| 309 |
+
// Backend Configuration
|
| 310 |
+
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
| 311 |
+
window.BACKEND_URL = `http://${window.location.hostname}:7860`;
|
| 312 |
+
} else {
|
| 313 |
+
window.BACKEND_URL = 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 314 |
+
}
|
| 315 |
+
</script>
|
| 316 |
+
|
| 317 |
+
<div class="app-shell">
|
| 318 |
+
<!-- Dynamic Sidebar -->
|
| 319 |
+
<aside class="sidebar" id="dynamicSidebar">
|
| 320 |
+
<div class="brand">
|
| 321 |
+
<div class="brand-icon">
|
| 322 |
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
| 323 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5"/>
|
| 324 |
+
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5"/>
|
| 325 |
+
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5"/>
|
| 326 |
+
</svg>
|
| 327 |
+
</div>
|
| 328 |
+
<div class="brand-text">
|
| 329 |
+
<strong>Crypto Intelligence</strong>
|
| 330 |
+
<span class="env-pill">
|
| 331 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none">
|
| 332 |
+
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
| 333 |
+
</svg>
|
| 334 |
+
Pro Edition
|
| 335 |
+
</span>
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
|
| 339 |
+
<nav class="nav" id="mainNav">
|
| 340 |
+
<button class="nav-button active" data-nav="page-overview">
|
| 341 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 342 |
+
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
|
| 343 |
+
</svg>
|
| 344 |
+
Overview
|
| 345 |
+
</button>
|
| 346 |
+
<button class="nav-button" data-nav="page-chart">
|
| 347 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 348 |
+
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/>
|
| 349 |
+
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/>
|
| 350 |
+
</svg>
|
| 351 |
+
Advanced Charts
|
| 352 |
+
</button>
|
| 353 |
+
<button class="nav-button" data-nav="page-compare">
|
| 354 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 355 |
+
<path d="M3 17l6-6 4 4 8-8" stroke="currentColor" stroke-width="2"/>
|
| 356 |
+
</svg>
|
| 357 |
+
Compare Coins
|
| 358 |
+
</button>
|
| 359 |
+
<button class="nav-button" data-nav="page-portfolio">
|
| 360 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 361 |
+
<path d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10l6 6v8a2 2 0 01-2 2z" stroke="currentColor" stroke-width="2"/>
|
| 362 |
+
</svg>
|
| 363 |
+
Portfolio
|
| 364 |
+
</button>
|
| 365 |
+
</nav>
|
| 366 |
+
|
| 367 |
+
<!-- Dynamic Stats -->
|
| 368 |
+
<div class="sidebar-stats" id="sidebarStats">
|
| 369 |
+
<div class="sidebar-stat-item">
|
| 370 |
+
<span class="sidebar-stat-label">Market Cap</span>
|
| 371 |
+
<span class="sidebar-stat-value" id="sidebarMarketCap">Loading...</span>
|
| 372 |
+
</div>
|
| 373 |
+
<div class="sidebar-stat-item">
|
| 374 |
+
<span class="sidebar-stat-label">24h Volume</span>
|
| 375 |
+
<span class="sidebar-stat-value" id="sidebarVolume">Loading...</span>
|
| 376 |
+
</div>
|
| 377 |
+
<div class="sidebar-stat-item">
|
| 378 |
+
<span class="sidebar-stat-label">BTC Price</span>
|
| 379 |
+
<span class="sidebar-stat-value positive" id="sidebarBTC">Loading...</span>
|
| 380 |
+
</div>
|
| 381 |
+
<div class="sidebar-stat-item">
|
| 382 |
+
<span class="sidebar-stat-label">ETH Price</span>
|
| 383 |
+
<span class="sidebar-stat-value positive" id="sidebarETH">Loading...</span>
|
| 384 |
+
</div>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<div class="sidebar-footer">
|
| 388 |
+
<div class="footer-badge">
|
| 389 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
| 390 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 391 |
+
<path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
|
| 392 |
+
</svg>
|
| 393 |
+
<span id="lastUpdate">Just now</span>
|
| 394 |
+
</div>
|
| 395 |
+
</div>
|
| 396 |
+
</aside>
|
| 397 |
+
|
| 398 |
+
<!-- Main Content -->
|
| 399 |
+
<main class="main-area">
|
| 400 |
+
<!-- Top Bar -->
|
| 401 |
+
<header class="topbar">
|
| 402 |
+
<div class="topbar-content">
|
| 403 |
+
<div class="topbar-icon">
|
| 404 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
|
| 405 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5"/>
|
| 406 |
+
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5"/>
|
| 407 |
+
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5"/>
|
| 408 |
+
</svg>
|
| 409 |
+
</div>
|
| 410 |
+
<div class="topbar-text">
|
| 411 |
+
<h1>
|
| 412 |
+
<span class="title-gradient">Professional</span>
|
| 413 |
+
<span class="title-accent">Dashboard</span>
|
| 414 |
+
</h1>
|
| 415 |
+
<p class="text-muted">
|
| 416 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="display: inline-block; vertical-align: middle; margin-right: 6px;">
|
| 417 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 418 |
+
<path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
|
| 419 |
+
</svg>
|
| 420 |
+
Real-time market data with advanced analytics
|
| 421 |
+
</p>
|
| 422 |
+
</div>
|
| 423 |
+
</div>
|
| 424 |
+
<div class="status-group">
|
| 425 |
+
<div class="status-pill" data-state="ok">
|
| 426 |
+
<span class="status-dot"></span>
|
| 427 |
+
<span class="status-label">API Connected</span>
|
| 428 |
+
</div>
|
| 429 |
+
<div class="status-pill" data-state="ok">
|
| 430 |
+
<span class="status-dot"></span>
|
| 431 |
+
<span class="status-label">Live Data</span>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
</header>
|
| 435 |
+
|
| 436 |
+
<div class="page-container">
|
| 437 |
+
<!-- Overview Page -->
|
| 438 |
+
<section id="page-overview" class="page active">
|
| 439 |
+
<div class="section-header">
|
| 440 |
+
<h2 class="section-title">Market Overview</h2>
|
| 441 |
+
<span class="chip">Real-time</span>
|
| 442 |
+
</div>
|
| 443 |
+
|
| 444 |
+
<!-- Stats Grid -->
|
| 445 |
+
<div class="stats-grid" id="statsGrid">
|
| 446 |
+
<!-- Stats will be dynamically loaded -->
|
| 447 |
+
</div>
|
| 448 |
+
|
| 449 |
+
<!-- Main Chart -->
|
| 450 |
+
<div class="glass-card" style="margin-top: var(--space-6);">
|
| 451 |
+
<div class="card-header">
|
| 452 |
+
<h4 class="card-title">
|
| 453 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
| 454 |
+
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/>
|
| 455 |
+
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/>
|
| 456 |
+
</svg>
|
| 457 |
+
Market Trends - Top 10 Cryptocurrencies
|
| 458 |
+
</h4>
|
| 459 |
+
<div style="display: flex; gap: var(--space-2);">
|
| 460 |
+
<span class="badge badge-cyan">24H</span>
|
| 461 |
+
<button class="btn-secondary btn-sm" onclick="refreshData()">
|
| 462 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
|
| 463 |
+
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2"/>
|
| 464 |
+
<path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" stroke="currentColor" stroke-width="2"/>
|
| 465 |
+
</svg>
|
| 466 |
+
Refresh
|
| 467 |
+
</button>
|
| 468 |
+
</div>
|
| 469 |
+
</div>
|
| 470 |
+
<div class="chart-container" style="height: 450px;">
|
| 471 |
+
<canvas id="mainChart"></canvas>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
|
| 475 |
+
<!-- Top Coins Table -->
|
| 476 |
+
<div class="glass-card" style="margin-top: var(--space-6);">
|
| 477 |
+
<div class="card-header">
|
| 478 |
+
<h4 class="card-title">
|
| 479 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
| 480 |
+
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="currentColor"/>
|
| 481 |
+
</svg>
|
| 482 |
+
Top Cryptocurrencies
|
| 483 |
+
</h4>
|
| 484 |
+
</div>
|
| 485 |
+
<div class="table-container">
|
| 486 |
+
<table class="table">
|
| 487 |
+
<thead>
|
| 488 |
+
<tr>
|
| 489 |
+
<th>#</th>
|
| 490 |
+
<th>Coin</th>
|
| 491 |
+
<th>Price</th>
|
| 492 |
+
<th>24h Change</th>
|
| 493 |
+
<th>7d Change</th>
|
| 494 |
+
<th>Market Cap</th>
|
| 495 |
+
<th>Volume (24h)</th>
|
| 496 |
+
<th>Last 7 Days</th>
|
| 497 |
+
</tr>
|
| 498 |
+
</thead>
|
| 499 |
+
<tbody id="topCoinsTable">
|
| 500 |
+
<!-- Data will be loaded dynamically -->
|
| 501 |
+
</tbody>
|
| 502 |
+
</table>
|
| 503 |
+
</div>
|
| 504 |
+
</div>
|
| 505 |
+
</section>
|
| 506 |
+
|
| 507 |
+
<!-- Advanced Charts Page -->
|
| 508 |
+
<section id="page-chart" class="page">
|
| 509 |
+
<div class="section-header">
|
| 510 |
+
<h2 class="section-title">Advanced Chart Analysis</h2>
|
| 511 |
+
<span class="chip">Interactive</span>
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
<!-- Chart Controls -->
|
| 515 |
+
<div class="chart-controls">
|
| 516 |
+
<div class="chart-control-group">
|
| 517 |
+
<label class="chart-control-label">
|
| 518 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 519 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 520 |
+
</svg>
|
| 521 |
+
Select Cryptocurrency
|
| 522 |
+
</label>
|
| 523 |
+
<div class="combobox-wrapper">
|
| 524 |
+
<input
|
| 525 |
+
type="text"
|
| 526 |
+
class="combobox-input"
|
| 527 |
+
id="coinSelector"
|
| 528 |
+
placeholder="Search for a coin..."
|
| 529 |
+
autocomplete="off"
|
| 530 |
+
/>
|
| 531 |
+
<svg class="combobox-icon" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 532 |
+
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
|
| 533 |
+
</svg>
|
| 534 |
+
<div class="combobox-dropdown" id="coinDropdown">
|
| 535 |
+
<!-- Options will be loaded dynamically -->
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
|
| 540 |
+
<div class="chart-control-group">
|
| 541 |
+
<label class="chart-control-label">
|
| 542 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 543 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 544 |
+
<path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2"/>
|
| 545 |
+
</svg>
|
| 546 |
+
Timeframe
|
| 547 |
+
</label>
|
| 548 |
+
<div class="chart-button-group">
|
| 549 |
+
<button class="chart-button" data-timeframe="1">1D</button>
|
| 550 |
+
<button class="chart-button active" data-timeframe="7">7D</button>
|
| 551 |
+
<button class="chart-button" data-timeframe="30">30D</button>
|
| 552 |
+
<button class="chart-button" data-timeframe="90">90D</button>
|
| 553 |
+
<button class="chart-button" data-timeframe="365">1Y</button>
|
| 554 |
+
</div>
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<div class="chart-control-group">
|
| 558 |
+
<label class="chart-control-label">
|
| 559 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 560 |
+
<path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" stroke="currentColor" stroke-width="2"/>
|
| 561 |
+
</svg>
|
| 562 |
+
Color Scheme
|
| 563 |
+
</label>
|
| 564 |
+
<div class="color-scheme-selector">
|
| 565 |
+
<div class="color-scheme-option color-scheme-blue active" data-scheme="blue"></div>
|
| 566 |
+
<div class="color-scheme-option color-scheme-purple" data-scheme="purple"></div>
|
| 567 |
+
<div class="color-scheme-option color-scheme-green" data-scheme="green"></div>
|
| 568 |
+
<div class="color-scheme-option color-scheme-orange" data-scheme="orange"></div>
|
| 569 |
+
<div class="color-scheme-option color-scheme-rainbow" data-scheme="rainbow"></div>
|
| 570 |
+
</div>
|
| 571 |
+
</div>
|
| 572 |
+
</div>
|
| 573 |
+
|
| 574 |
+
<!-- Price Chart -->
|
| 575 |
+
<div class="glass-card">
|
| 576 |
+
<div class="card-header">
|
| 577 |
+
<h4 class="card-title" id="chartTitle">Bitcoin (BTC) Price Chart</h4>
|
| 578 |
+
<div style="display: flex; gap: var(--space-2); align-items: center;">
|
| 579 |
+
<span class="badge badge-success" id="chartPrice">$0</span>
|
| 580 |
+
<span class="badge badge-cyan" id="chartChange">0%</span>
|
| 581 |
+
</div>
|
| 582 |
+
</div>
|
| 583 |
+
<div class="chart-container" style="height: 500px;">
|
| 584 |
+
<canvas id="priceChart"></canvas>
|
| 585 |
+
</div>
|
| 586 |
+
</div>
|
| 587 |
+
|
| 588 |
+
<!-- Volume Chart -->
|
| 589 |
+
<div class="glass-card" style="margin-top: var(--space-6);">
|
| 590 |
+
<div class="card-header">
|
| 591 |
+
<h4 class="card-title">Trading Volume</h4>
|
| 592 |
+
</div>
|
| 593 |
+
<div class="chart-container" style="height: 300px;">
|
| 594 |
+
<canvas id="volumeChart"></canvas>
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
</section>
|
| 598 |
+
|
| 599 |
+
<!-- Compare Page -->
|
| 600 |
+
<section id="page-compare" class="page">
|
| 601 |
+
<div class="section-header">
|
| 602 |
+
<h2 class="section-title">Compare Cryptocurrencies</h2>
|
| 603 |
+
<span class="chip">Side by Side</span>
|
| 604 |
+
</div>
|
| 605 |
+
|
| 606 |
+
<div class="alert alert-info">
|
| 607 |
+
<svg class="alert-icon" width="20" height="20" viewBox="0 0 24 24" fill="none">
|
| 608 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 609 |
+
<path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2"/>
|
| 610 |
+
</svg>
|
| 611 |
+
<div class="alert-content">
|
| 612 |
+
<div class="alert-title">Compare up to 5 cryptocurrencies</div>
|
| 613 |
+
<div class="alert-description">Select coins to compare their performance side by side</div>
|
| 614 |
+
</div>
|
| 615 |
+
</div>
|
| 616 |
+
|
| 617 |
+
<div class="glass-card" style="margin-top: var(--space-6);">
|
| 618 |
+
<div class="card-header">
|
| 619 |
+
<h4 class="card-title">Comparison Chart</h4>
|
| 620 |
+
</div>
|
| 621 |
+
<div class="chart-container" style="height: 450px;">
|
| 622 |
+
<canvas id="compareChart"></canvas>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
</section>
|
| 626 |
+
|
| 627 |
+
<!-- Portfolio Page -->
|
| 628 |
+
<section id="page-portfolio" class="page">
|
| 629 |
+
<div class="section-header">
|
| 630 |
+
<h2 class="section-title">Portfolio Tracker</h2>
|
| 631 |
+
<button class="btn-primary">
|
| 632 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 633 |
+
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2"/>
|
| 634 |
+
</svg>
|
| 635 |
+
Add Asset
|
| 636 |
+
</button>
|
| 637 |
+
</div>
|
| 638 |
+
|
| 639 |
+
<div class="empty-state">
|
| 640 |
+
<div class="empty-state-icon">📊</div>
|
| 641 |
+
<div class="empty-state-title">No Portfolio Data</div>
|
| 642 |
+
<div class="empty-state-description">
|
| 643 |
+
Start tracking your crypto portfolio by adding your first asset
|
| 644 |
+
</div>
|
| 645 |
+
<button class="btn-primary" style="margin-top: var(--space-4);">
|
| 646 |
+
Get Started
|
| 647 |
+
</button>
|
| 648 |
+
</div>
|
| 649 |
+
</section>
|
| 650 |
+
</div>
|
| 651 |
+
</main>
|
| 652 |
+
</div>
|
| 653 |
+
|
| 654 |
+
<!-- Load App JS -->
|
| 655 |
+
<script type="module" src="static/js/app-pro.js"></script>
|
| 656 |
+
</body>
|
| 657 |
+
</html>
|
archive_html/complete_dashboard.html
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto API Monitor - Complete Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 16 |
+
background: #0f172a;
|
| 17 |
+
color: #e2e8f0;
|
| 18 |
+
overflow-x: hidden;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.header {
|
| 22 |
+
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
| 23 |
+
padding: 20px 40px;
|
| 24 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
| 25 |
+
position: sticky;
|
| 26 |
+
top: 0;
|
| 27 |
+
z-index: 1000;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.header-content {
|
| 31 |
+
max-width: 1600px;
|
| 32 |
+
margin: 0 auto;
|
| 33 |
+
display: flex;
|
| 34 |
+
justify-content: space-between;
|
| 35 |
+
align-items: center;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.logo {
|
| 39 |
+
font-size: 24px;
|
| 40 |
+
font-weight: bold;
|
| 41 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
| 42 |
+
-webkit-background-clip: text;
|
| 43 |
+
-webkit-text-fill-color: transparent;
|
| 44 |
+
display: flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
gap: 10px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.header-actions {
|
| 50 |
+
display: flex;
|
| 51 |
+
gap: 15px;
|
| 52 |
+
align-items: center;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.btn {
|
| 56 |
+
padding: 10px 20px;
|
| 57 |
+
border: none;
|
| 58 |
+
border-radius: 8px;
|
| 59 |
+
font-weight: 600;
|
| 60 |
+
cursor: pointer;
|
| 61 |
+
transition: all 0.3s ease;
|
| 62 |
+
font-size: 14px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.btn-primary {
|
| 66 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
| 67 |
+
color: white;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.btn-primary:hover {
|
| 71 |
+
transform: translateY(-2px);
|
| 72 |
+
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.btn-secondary {
|
| 76 |
+
background: #334155;
|
| 77 |
+
color: #e2e8f0;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.btn-secondary:hover {
|
| 81 |
+
background: #475569;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.container {
|
| 85 |
+
max-width: 1600px;
|
| 86 |
+
margin: 0 auto;
|
| 87 |
+
padding: 30px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.tabs {
|
| 91 |
+
display: flex;
|
| 92 |
+
gap: 5px;
|
| 93 |
+
margin-bottom: 30px;
|
| 94 |
+
background: #1e293b;
|
| 95 |
+
padding: 10px;
|
| 96 |
+
border-radius: 12px;
|
| 97 |
+
overflow-x: auto;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.tab {
|
| 101 |
+
padding: 12px 24px;
|
| 102 |
+
border: none;
|
| 103 |
+
background: transparent;
|
| 104 |
+
color: #94a3b8;
|
| 105 |
+
cursor: pointer;
|
| 106 |
+
border-radius: 8px;
|
| 107 |
+
font-weight: 600;
|
| 108 |
+
transition: all 0.3s ease;
|
| 109 |
+
white-space: nowrap;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.tab:hover {
|
| 113 |
+
background: #334155;
|
| 114 |
+
color: #e2e8f0;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.tab.active {
|
| 118 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
| 119 |
+
color: white;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.tab-content {
|
| 123 |
+
display: none;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.tab-content.active {
|
| 127 |
+
display: block;
|
| 128 |
+
animation: fadeIn 0.3s ease;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
@keyframes fadeIn {
|
| 132 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 133 |
+
to { opacity: 1; transform: translateY(0); }
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.stats-grid {
|
| 137 |
+
display: grid;
|
| 138 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 139 |
+
gap: 20px;
|
| 140 |
+
margin-bottom: 30px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.stat-card {
|
| 144 |
+
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
| 145 |
+
padding: 25px;
|
| 146 |
+
border-radius: 12px;
|
| 147 |
+
border: 1px solid #334155;
|
| 148 |
+
transition: all 0.3s ease;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.stat-card:hover {
|
| 152 |
+
transform: translateY(-5px);
|
| 153 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 154 |
+
border-color: #3b82f6;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.stat-card h3 {
|
| 158 |
+
color: #94a3b8;
|
| 159 |
+
font-size: 14px;
|
| 160 |
+
text-transform: uppercase;
|
| 161 |
+
margin-bottom: 10px;
|
| 162 |
+
font-weight: 600;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.stat-card .value {
|
| 166 |
+
font-size: 36px;
|
| 167 |
+
font-weight: bold;
|
| 168 |
+
margin-bottom: 5px;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.stat-card .label {
|
| 172 |
+
color: #64748b;
|
| 173 |
+
font-size: 13px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.stat-card.green .value { color: #10b981; }
|
| 177 |
+
.stat-card.blue .value { color: #3b82f6; }
|
| 178 |
+
.stat-card.purple .value { color: #8b5cf6; }
|
| 179 |
+
.stat-card.orange .value { color: #f59e0b; }
|
| 180 |
+
.stat-card.red .value { color: #ef4444; }
|
| 181 |
+
|
| 182 |
+
.card {
|
| 183 |
+
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
| 184 |
+
border-radius: 12px;
|
| 185 |
+
padding: 25px;
|
| 186 |
+
border: 1px solid #334155;
|
| 187 |
+
margin-bottom: 20px;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.card h2 {
|
| 191 |
+
color: #e2e8f0;
|
| 192 |
+
margin-bottom: 20px;
|
| 193 |
+
font-size: 20px;
|
| 194 |
+
display: flex;
|
| 195 |
+
align-items: center;
|
| 196 |
+
gap: 10px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.providers-grid {
|
| 200 |
+
display: grid;
|
| 201 |
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 202 |
+
gap: 15px;
|
| 203 |
+
max-height: 600px;
|
| 204 |
+
overflow-y: auto;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.provider-card {
|
| 208 |
+
background: #0f172a;
|
| 209 |
+
border-radius: 8px;
|
| 210 |
+
padding: 15px;
|
| 211 |
+
border-left: 4px solid #334155;
|
| 212 |
+
transition: all 0.3s ease;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.provider-card:hover {
|
| 216 |
+
transform: translateX(5px);
|
| 217 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.provider-card.online {
|
| 221 |
+
border-left-color: #10b981;
|
| 222 |
+
background: linear-gradient(to right, rgba(16, 185, 129, 0.1), #0f172a);
|
| 223 |
+
}
|
| 224 |
+
.provider-card.offline {
|
| 225 |
+
border-left-color: #ef4444;
|
| 226 |
+
background: linear-gradient(to right, rgba(239, 68, 68, 0.1), #0f172a);
|
| 227 |
+
}
|
| 228 |
+
.provider-card.degraded {
|
| 229 |
+
border-left-color: #f59e0b;
|
| 230 |
+
background: linear-gradient(to right, rgba(245, 158, 11, 0.1), #0f172a);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.provider-card .name {
|
| 234 |
+
font-weight: 600;
|
| 235 |
+
color: #e2e8f0;
|
| 236 |
+
margin-bottom: 8px;
|
| 237 |
+
font-size: 15px;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
.provider-card .category {
|
| 241 |
+
font-size: 12px;
|
| 242 |
+
color: #94a3b8;
|
| 243 |
+
margin-bottom: 8px;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.provider-card .status {
|
| 247 |
+
display: inline-block;
|
| 248 |
+
padding: 4px 10px;
|
| 249 |
+
border-radius: 6px;
|
| 250 |
+
font-size: 11px;
|
| 251 |
+
font-weight: 600;
|
| 252 |
+
text-transform: uppercase;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.status.online { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
| 256 |
+
.status.offline { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
| 257 |
+
.status.degraded { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
| 258 |
+
|
| 259 |
+
.provider-card .response-time {
|
| 260 |
+
font-size: 12px;
|
| 261 |
+
color: #64748b;
|
| 262 |
+
margin-top: 8px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.category-list {
|
| 266 |
+
display: grid;
|
| 267 |
+
gap: 15px;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.category-item {
|
| 271 |
+
background: #0f172a;
|
| 272 |
+
border-radius: 8px;
|
| 273 |
+
padding: 20px;
|
| 274 |
+
border-left: 4px solid #3b82f6;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.category-item .name {
|
| 278 |
+
font-weight: 600;
|
| 279 |
+
color: #e2e8f0;
|
| 280 |
+
margin-bottom: 12px;
|
| 281 |
+
font-size: 16px;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.category-item .stats {
|
| 285 |
+
display: flex;
|
| 286 |
+
gap: 20px;
|
| 287 |
+
font-size: 14px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.category-item .stat {
|
| 291 |
+
display: flex;
|
| 292 |
+
align-items: center;
|
| 293 |
+
gap: 6px;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.loading {
|
| 297 |
+
text-align: center;
|
| 298 |
+
padding: 60px;
|
| 299 |
+
color: #64748b;
|
| 300 |
+
font-size: 16px;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.spinner {
|
| 304 |
+
border: 3px solid #334155;
|
| 305 |
+
border-top: 3px solid #3b82f6;
|
| 306 |
+
border-radius: 50%;
|
| 307 |
+
width: 40px;
|
| 308 |
+
height: 40px;
|
| 309 |
+
animation: spin 1s linear infinite;
|
| 310 |
+
margin: 20px auto;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
@keyframes spin {
|
| 314 |
+
0% { transform: rotate(0deg); }
|
| 315 |
+
100% { transform: rotate(360deg); }
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.grid-2 {
|
| 319 |
+
display: grid;
|
| 320 |
+
grid-template-columns: 2fr 1fr;
|
| 321 |
+
gap: 20px;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.market-item {
|
| 325 |
+
display: flex;
|
| 326 |
+
justify-content: space-between;
|
| 327 |
+
align-items: center;
|
| 328 |
+
padding: 15px;
|
| 329 |
+
background: #0f172a;
|
| 330 |
+
border-radius: 8px;
|
| 331 |
+
margin-bottom: 10px;
|
| 332 |
+
border: 1px solid #334155;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.market-item:hover {
|
| 336 |
+
border-color: #3b82f6;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.market-item .coin-info {
|
| 340 |
+
display: flex;
|
| 341 |
+
align-items: center;
|
| 342 |
+
gap: 15px;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.market-item .coin-name {
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
color: #e2e8f0;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.market-item .coin-symbol {
|
| 351 |
+
color: #94a3b8;
|
| 352 |
+
font-size: 14px;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.market-item .price {
|
| 356 |
+
font-size: 18px;
|
| 357 |
+
font-weight: 600;
|
| 358 |
+
color: #3b82f6;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.market-item .change {
|
| 362 |
+
padding: 4px 10px;
|
| 363 |
+
border-radius: 6px;
|
| 364 |
+
font-weight: 600;
|
| 365 |
+
font-size: 14px;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.market-item .change.positive {
|
| 369 |
+
background: rgba(16, 185, 129, 0.2);
|
| 370 |
+
color: #10b981;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.market-item .change.negative {
|
| 374 |
+
background: rgba(239, 68, 68, 0.2);
|
| 375 |
+
color: #ef4444;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.search-box {
|
| 379 |
+
width: 100%;
|
| 380 |
+
padding: 12px 20px;
|
| 381 |
+
background: #0f172a;
|
| 382 |
+
border: 1px solid #334155;
|
| 383 |
+
border-radius: 8px;
|
| 384 |
+
color: #e2e8f0;
|
| 385 |
+
font-size: 14px;
|
| 386 |
+
margin-bottom: 20px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.search-box:focus {
|
| 390 |
+
outline: none;
|
| 391 |
+
border-color: #3b82f6;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
::-webkit-scrollbar {
|
| 395 |
+
width: 8px;
|
| 396 |
+
height: 8px;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
::-webkit-scrollbar-track {
|
| 400 |
+
background: #1e293b;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
::-webkit-scrollbar-thumb {
|
| 404 |
+
background: #334155;
|
| 405 |
+
border-radius: 4px;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
::-webkit-scrollbar-thumb:hover {
|
| 409 |
+
background: #475569;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
@media (max-width: 768px) {
|
| 413 |
+
.grid-2 {
|
| 414 |
+
grid-template-columns: 1fr;
|
| 415 |
+
}
|
| 416 |
+
.stats-grid {
|
| 417 |
+
grid-template-columns: 1fr;
|
| 418 |
+
}
|
| 419 |
+
.header-content {
|
| 420 |
+
flex-direction: column;
|
| 421 |
+
gap: 15px;
|
| 422 |
+
}
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.status-indicator {
|
| 426 |
+
display: inline-block;
|
| 427 |
+
width: 8px;
|
| 428 |
+
height: 8px;
|
| 429 |
+
border-radius: 50%;
|
| 430 |
+
margin-right: 8px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.status-indicator.online { background: #10b981; }
|
| 434 |
+
.status-indicator.offline { background: #ef4444; }
|
| 435 |
+
.status-indicator.degraded { background: #f59e0b; }
|
| 436 |
+
|
| 437 |
+
.empty-state {
|
| 438 |
+
text-align: center;
|
| 439 |
+
padding: 60px 20px;
|
| 440 |
+
color: #64748b;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.empty-state h3 {
|
| 444 |
+
color: #94a3b8;
|
| 445 |
+
margin-bottom: 10px;
|
| 446 |
+
}
|
| 447 |
+
</style>
|
| 448 |
+
</head>
|
| 449 |
+
<body>
|
| 450 |
+
<div class="header">
|
| 451 |
+
<div class="header-content">
|
| 452 |
+
<div class="logo">
|
| 453 |
+
<span>🚀</span>
|
| 454 |
+
<span>Crypto API Monitor</span>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="header-actions">
|
| 457 |
+
<span id="connectionStatus" style="color: #64748b; font-size: 14px;">
|
| 458 |
+
<span class="status-indicator online"></span>
|
| 459 |
+
Connected
|
| 460 |
+
</span>
|
| 461 |
+
<button class="btn btn-secondary" onclick="refreshData()">🔄 Refresh</button>
|
| 462 |
+
<button class="btn btn-primary" onclick="exportData()">💾 Export</button>
|
| 463 |
+
</div>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<div class="container">
|
| 468 |
+
<div class="tabs">
|
| 469 |
+
<button class="tab active" onclick="switchTab('overview')">📊 Overview</button>
|
| 470 |
+
<button class="tab" onclick="switchTab('providers')">🔌 Providers</button>
|
| 471 |
+
<button class="tab" onclick="switchTab('categories')">📁 Categories</button>
|
| 472 |
+
<button class="tab" onclick="switchTab('market')">💰 Market Data</button>
|
| 473 |
+
<button class="tab" onclick="switchTab('health')">❤️ Health</button>
|
| 474 |
+
</div>
|
| 475 |
+
|
| 476 |
+
<!-- Overview Tab -->
|
| 477 |
+
<div id="overview" class="tab-content active">
|
| 478 |
+
<div class="stats-grid">
|
| 479 |
+
<div class="stat-card blue">
|
| 480 |
+
<h3>Total Providers</h3>
|
| 481 |
+
<div class="value" id="totalProviders">-</div>
|
| 482 |
+
<div class="label">API Sources</div>
|
| 483 |
+
</div>
|
| 484 |
+
<div class="stat-card green">
|
| 485 |
+
<h3>Online</h3>
|
| 486 |
+
<div class="value" id="onlineProviders">-</div>
|
| 487 |
+
<div class="label">Working Perfectly</div>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="stat-card orange">
|
| 490 |
+
<h3>Degraded</h3>
|
| 491 |
+
<div class="value" id="degradedProviders">-</div>
|
| 492 |
+
<div class="label">Slow Response</div>
|
| 493 |
+
</div>
|
| 494 |
+
<div class="stat-card red">
|
| 495 |
+
<h3>Offline</h3>
|
| 496 |
+
<div class="value" id="offlineProviders">-</div>
|
| 497 |
+
<div class="label">Not Responding</div>
|
| 498 |
+
</div>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<div class="grid-2">
|
| 502 |
+
<div class="card">
|
| 503 |
+
<h2>🔌 Recent Provider Status</h2>
|
| 504 |
+
<div id="recentProviders">
|
| 505 |
+
<div class="loading">
|
| 506 |
+
<div class="spinner"></div>
|
| 507 |
+
Loading providers...
|
| 508 |
+
</div>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
|
| 512 |
+
<div class="card">
|
| 513 |
+
<h2>📈 System Health</h2>
|
| 514 |
+
<div id="systemHealth">
|
| 515 |
+
<div class="loading">
|
| 516 |
+
<div class="spinner"></div>
|
| 517 |
+
Loading health data...
|
| 518 |
+
</div>
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<!-- Providers Tab -->
|
| 525 |
+
<div id="providers" class="tab-content">
|
| 526 |
+
<div class="card">
|
| 527 |
+
<h2>🔌 All Providers</h2>
|
| 528 |
+
<input type="text" class="search-box" id="providerSearch" placeholder="Search providers..." onkeyup="filterProviders()">
|
| 529 |
+
<div class="providers-grid" id="allProviders">
|
| 530 |
+
<div class="loading">
|
| 531 |
+
<div class="spinner"></div>
|
| 532 |
+
Loading providers...
|
| 533 |
+
</div>
|
| 534 |
+
</div>
|
| 535 |
+
</div>
|
| 536 |
+
</div>
|
| 537 |
+
|
| 538 |
+
<!-- Categories Tab -->
|
| 539 |
+
<div id="categories" class="tab-content">
|
| 540 |
+
<div class="card">
|
| 541 |
+
<h2>📁 Categories Breakdown</h2>
|
| 542 |
+
<div class="category-list" id="categoriesList">
|
| 543 |
+
<div class="loading">
|
| 544 |
+
<div class="spinner"></div>
|
| 545 |
+
Loading categories...
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
</div>
|
| 550 |
+
|
| 551 |
+
<!-- Market Tab -->
|
| 552 |
+
<div id="market" class="tab-content">
|
| 553 |
+
<div class="card">
|
| 554 |
+
<h2>💰 Market Data</h2>
|
| 555 |
+
<div id="marketData">
|
| 556 |
+
<div class="loading">
|
| 557 |
+
<div class="spinner"></div>
|
| 558 |
+
Loading market data...
|
| 559 |
+
</div>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
|
| 564 |
+
<!-- Health Tab -->
|
| 565 |
+
<div id="health" class="tab-content">
|
| 566 |
+
<div class="stats-grid">
|
| 567 |
+
<div class="stat-card purple">
|
| 568 |
+
<h3>Uptime</h3>
|
| 569 |
+
<div class="value" id="uptimePercent">-</div>
|
| 570 |
+
<div class="label">Overall Health</div>
|
| 571 |
+
</div>
|
| 572 |
+
<div class="stat-card blue">
|
| 573 |
+
<h3>Avg Response</h3>
|
| 574 |
+
<div class="value" id="avgResponse">-</div>
|
| 575 |
+
<div class="label">Milliseconds</div>
|
| 576 |
+
</div>
|
| 577 |
+
<div class="stat-card green">
|
| 578 |
+
<h3>Categories</h3>
|
| 579 |
+
<div class="value" id="totalCategories">-</div>
|
| 580 |
+
<div class="label">Data Types</div>
|
| 581 |
+
</div>
|
| 582 |
+
<div class="stat-card orange">
|
| 583 |
+
<h3>Last Check</h3>
|
| 584 |
+
<div class="value" id="lastCheck" style="font-size: 18px;">-</div>
|
| 585 |
+
<div class="label">Timestamp</div>
|
| 586 |
+
</div>
|
| 587 |
+
</div>
|
| 588 |
+
|
| 589 |
+
<div class="card">
|
| 590 |
+
<h2>📊 Detailed Health Report</h2>
|
| 591 |
+
<div id="healthDetails">
|
| 592 |
+
<div class="loading">
|
| 593 |
+
<div class="spinner"></div>
|
| 594 |
+
Loading health details...
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
</div>
|
| 598 |
+
</div>
|
| 599 |
+
</div>
|
| 600 |
+
|
| 601 |
+
<script>
|
| 602 |
+
let allProvidersData = [];
|
| 603 |
+
let currentTab = 'overview';
|
| 604 |
+
|
| 605 |
+
function switchTab(tabName) {
|
| 606 |
+
// Hide all tabs
|
| 607 |
+
document.querySelectorAll('.tab-content').forEach(tab => {
|
| 608 |
+
tab.classList.remove('active');
|
| 609 |
+
});
|
| 610 |
+
document.querySelectorAll('.tab').forEach(tab => {
|
| 611 |
+
tab.classList.remove('active');
|
| 612 |
+
});
|
| 613 |
+
|
| 614 |
+
// Show selected tab
|
| 615 |
+
document.getElementById(tabName).classList.add('active');
|
| 616 |
+
event.target.classList.add('active');
|
| 617 |
+
currentTab = tabName;
|
| 618 |
+
|
| 619 |
+
// Load data for specific tabs
|
| 620 |
+
if (tabName === 'market') {
|
| 621 |
+
loadMarketData();
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
async function loadProviders() {
|
| 626 |
+
try {
|
| 627 |
+
const response = await fetch('/api/providers');
|
| 628 |
+
const providers = await response.json();
|
| 629 |
+
allProvidersData = providers;
|
| 630 |
+
|
| 631 |
+
// Calculate stats
|
| 632 |
+
const online = providers.filter(p => p.status === 'online').length;
|
| 633 |
+
const offline = providers.filter(p => p.status === 'offline').length;
|
| 634 |
+
const degraded = providers.filter(p => p.status === 'degraded').length;
|
| 635 |
+
const uptime = ((online / providers.length) * 100).toFixed(1);
|
| 636 |
+
|
| 637 |
+
// Update stats
|
| 638 |
+
document.getElementById('totalProviders').textContent = providers.length;
|
| 639 |
+
document.getElementById('onlineProviders').textContent = online;
|
| 640 |
+
document.getElementById('degradedProviders').textContent = degraded;
|
| 641 |
+
document.getElementById('offlineProviders').textContent = offline;
|
| 642 |
+
document.getElementById('uptimePercent').textContent = uptime + '%';
|
| 643 |
+
|
| 644 |
+
// Calculate average response time
|
| 645 |
+
const responseTimes = providers.filter(p => p.response_time_ms).map(p => p.response_time_ms);
|
| 646 |
+
const avgResp = responseTimes.length > 0 ?
|
| 647 |
+
Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0;
|
| 648 |
+
document.getElementById('avgResponse').textContent = avgResp + 'ms';
|
| 649 |
+
|
| 650 |
+
// Group by category
|
| 651 |
+
const categories = {};
|
| 652 |
+
providers.forEach(p => {
|
| 653 |
+
if (!categories[p.category]) {
|
| 654 |
+
categories[p.category] = { total: 0, online: 0, offline: 0, degraded: 0 };
|
| 655 |
+
}
|
| 656 |
+
categories[p.category].total++;
|
| 657 |
+
categories[p.category][p.status]++;
|
| 658 |
+
});
|
| 659 |
+
|
| 660 |
+
document.getElementById('totalCategories').textContent = Object.keys(categories).length;
|
| 661 |
+
document.getElementById('lastCheck').textContent = new Date().toLocaleTimeString();
|
| 662 |
+
|
| 663 |
+
// Display recent providers (first 10)
|
| 664 |
+
displayRecentProviders(providers.slice(0, 10));
|
| 665 |
+
|
| 666 |
+
// Display all providers
|
| 667 |
+
displayAllProviders(providers);
|
| 668 |
+
|
| 669 |
+
// Display categories
|
| 670 |
+
displayCategories(categories);
|
| 671 |
+
|
| 672 |
+
// Display health details
|
| 673 |
+
displayHealthDetails(providers);
|
| 674 |
+
|
| 675 |
+
} catch (error) {
|
| 676 |
+
console.error('Error loading providers:', error);
|
| 677 |
+
}
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
function displayRecentProviders(providers) {
|
| 681 |
+
const html = providers.map(p => `
|
| 682 |
+
<div class="provider-card ${p.status}">
|
| 683 |
+
<div class="name">${p.name}</div>
|
| 684 |
+
<div class="category">${p.category}</div>
|
| 685 |
+
<span class="status ${p.status}">${p.status}</span>
|
| 686 |
+
${p.response_time_ms ? `<div class="response-time">${Math.round(p.response_time_ms)}ms</div>` : ''}
|
| 687 |
+
</div>
|
| 688 |
+
`).join('');
|
| 689 |
+
document.getElementById('recentProviders').innerHTML = `<div class="providers-grid" style="max-height: 400px;">${html}</div>`;
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
function displayAllProviders(providers) {
|
| 693 |
+
const html = providers.map(p => `
|
| 694 |
+
<div class="provider-card ${p.status}" data-name="${p.name.toLowerCase()}" data-category="${p.category.toLowerCase()}">
|
| 695 |
+
<div class="name">${p.name}</div>
|
| 696 |
+
<div class="category">${p.category}</div>
|
| 697 |
+
<span class="status ${p.status}">${p.status}</span>
|
| 698 |
+
${p.response_time_ms ? `<div class="response-time">⚡ ${Math.round(p.response_time_ms)}ms</div>` : ''}
|
| 699 |
+
</div>
|
| 700 |
+
`).join('');
|
| 701 |
+
document.getElementById('allProviders').innerHTML = html;
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
function displayCategories(categories) {
|
| 705 |
+
const html = Object.entries(categories).map(([name, stats]) => `
|
| 706 |
+
<div class="category-item">
|
| 707 |
+
<div class="name">${name}</div>
|
| 708 |
+
<div class="stats">
|
| 709 |
+
<div class="stat">
|
| 710 |
+
<span class="status-indicator online"></span>
|
| 711 |
+
<span>${stats.online} online</span>
|
| 712 |
+
</div>
|
| 713 |
+
<div class="stat">
|
| 714 |
+
<span class="status-indicator degraded"></span>
|
| 715 |
+
<span>${stats.degraded} degraded</span>
|
| 716 |
+
</div>
|
| 717 |
+
<div class="stat">
|
| 718 |
+
<span class="status-indicator offline"></span>
|
| 719 |
+
<span>${stats.offline} offline</span>
|
| 720 |
+
</div>
|
| 721 |
+
<div class="stat" style="margin-left: auto; color: #3b82f6; font-weight: 600;">
|
| 722 |
+
${stats.total} total
|
| 723 |
+
</div>
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
`).join('');
|
| 727 |
+
document.getElementById('categoriesList').innerHTML = html;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
function displayHealthDetails(providers) {
|
| 731 |
+
const online = providers.filter(p => p.status === 'online');
|
| 732 |
+
const degraded = providers.filter(p => p.status === 'degraded');
|
| 733 |
+
const offline = providers.filter(p => p.status === 'offline');
|
| 734 |
+
|
| 735 |
+
const html = `
|
| 736 |
+
<div style="display: grid; gap: 20px;">
|
| 737 |
+
<div class="category-item" style="border-left-color: #10b981;">
|
| 738 |
+
<div class="name">✅ Online Providers (${online.length})</div>
|
| 739 |
+
<div style="color: #94a3b8; margin-top: 10px;">
|
| 740 |
+
${online.slice(0, 5).map(p => p.name).join(', ')}
|
| 741 |
+
${online.length > 5 ? ` and ${online.length - 5} more...` : ''}
|
| 742 |
+
</div>
|
| 743 |
+
</div>
|
| 744 |
+
<div class="category-item" style="border-left-color: #f59e0b;">
|
| 745 |
+
<div class="name">⚠️ Degraded Providers (${degraded.length})</div>
|
| 746 |
+
<div style="color: #94a3b8; margin-top: 10px;">
|
| 747 |
+
${degraded.length > 0 ? degraded.map(p => p.name).join(', ') : 'None'}
|
| 748 |
+
</div>
|
| 749 |
+
</div>
|
| 750 |
+
<div class="category-item" style="border-left-color: #ef4444;">
|
| 751 |
+
<div class="name">❌ Offline Providers (${offline.length})</div>
|
| 752 |
+
<div style="color: #94a3b8; margin-top: 10px;">
|
| 753 |
+
${offline.length > 0 ? offline.map(p => p.name).join(', ') : 'None'}
|
| 754 |
+
</div>
|
| 755 |
+
</div>
|
| 756 |
+
</div>
|
| 757 |
+
`;
|
| 758 |
+
document.getElementById('healthDetails').innerHTML = html;
|
| 759 |
+
|
| 760 |
+
const systemHealthHtml = `
|
| 761 |
+
<div style="display: grid; gap: 15px;">
|
| 762 |
+
<div style="padding: 15px; background: #0f172a; border-radius: 8px;">
|
| 763 |
+
<div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">System Status</div>
|
| 764 |
+
<div style="font-size: 24px; font-weight: 600; color: ${online.length >= providers.length * 0.7 ? '#10b981' : '#f59e0b'};">
|
| 765 |
+
${online.length >= providers.length * 0.7 ? '✅ Healthy' : '⚠️ Degraded'}
|
| 766 |
+
</div>
|
| 767 |
+
</div>
|
| 768 |
+
<div style="padding: 15px; background: #0f172a; border-radius: 8px;">
|
| 769 |
+
<div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Availability</div>
|
| 770 |
+
<div style="font-size: 24px; font-weight: 600; color: #3b82f6;">
|
| 771 |
+
${((online.length / providers.length) * 100).toFixed(1)}%
|
| 772 |
+
</div>
|
| 773 |
+
</div>
|
| 774 |
+
<div style="padding: 15px; background: #0f172a; border-radius: 8px;">
|
| 775 |
+
<div style="color: #94a3b8; font-size: 14px; margin-bottom: 5px;">Total Checks</div>
|
| 776 |
+
<div style="font-size: 24px; font-weight: 600; color: #8b5cf6;">
|
| 777 |
+
${providers.length}
|
| 778 |
+
</div>
|
| 779 |
+
</div>
|
| 780 |
+
</div>
|
| 781 |
+
`;
|
| 782 |
+
document.getElementById('systemHealth').innerHTML = systemHealthHtml;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
async function loadMarketData() {
|
| 786 |
+
try {
|
| 787 |
+
const response = await fetch('/api/market');
|
| 788 |
+
const data = await response.json();
|
| 789 |
+
|
| 790 |
+
if (data.cryptocurrencies && data.cryptocurrencies.length > 0) {
|
| 791 |
+
const html = data.cryptocurrencies.map(coin => `
|
| 792 |
+
<div class="market-item">
|
| 793 |
+
<div class="coin-info">
|
| 794 |
+
<div>
|
| 795 |
+
<div class="coin-name">${coin.name}</div>
|
| 796 |
+
<div class="coin-symbol">${coin.symbol}</div>
|
| 797 |
+
</div>
|
| 798 |
+
</div>
|
| 799 |
+
<div style="display: flex; align-items: center; gap: 20px;">
|
| 800 |
+
<div class="price">$${coin.price.toLocaleString()}</div>
|
| 801 |
+
<div class="change ${coin.change_24h >= 0 ? 'positive' : 'negative'}">
|
| 802 |
+
${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}%
|
| 803 |
+
</div>
|
| 804 |
+
</div>
|
| 805 |
+
</div>
|
| 806 |
+
`).join('');
|
| 807 |
+
document.getElementById('marketData').innerHTML = html;
|
| 808 |
+
} else {
|
| 809 |
+
document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>No market data available</h3><p>Market data providers may be offline</p></div>';
|
| 810 |
+
}
|
| 811 |
+
} catch (error) {
|
| 812 |
+
console.error('Error loading market data:', error);
|
| 813 |
+
document.getElementById('marketData').innerHTML = '<div class="empty-state"><h3>Error loading market data</h3></div>';
|
| 814 |
+
}
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
function filterProviders() {
|
| 818 |
+
const search = document.getElementById('providerSearch').value.toLowerCase();
|
| 819 |
+
const cards = document.querySelectorAll('#allProviders .provider-card');
|
| 820 |
+
|
| 821 |
+
cards.forEach(card => {
|
| 822 |
+
const name = card.getAttribute('data-name');
|
| 823 |
+
const category = card.getAttribute('data-category');
|
| 824 |
+
if (name.includes(search) || category.includes(search)) {
|
| 825 |
+
card.style.display = 'block';
|
| 826 |
+
} else {
|
| 827 |
+
card.style.display = 'none';
|
| 828 |
+
}
|
| 829 |
+
});
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
function refreshData() {
|
| 833 |
+
loadProviders();
|
| 834 |
+
if (currentTab === 'market') {
|
| 835 |
+
loadMarketData();
|
| 836 |
+
}
|
| 837 |
+
}
|
| 838 |
+
|
| 839 |
+
function exportData() {
|
| 840 |
+
const dataStr = JSON.stringify(allProvidersData, null, 2);
|
| 841 |
+
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
| 842 |
+
const url = URL.createObjectURL(dataBlob);
|
| 843 |
+
const link = document.createElement('a');
|
| 844 |
+
link.href = url;
|
| 845 |
+
link.download = `crypto-monitor-${new Date().toISOString()}.json`;
|
| 846 |
+
link.click();
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
// Load data on start
|
| 850 |
+
loadProviders();
|
| 851 |
+
|
| 852 |
+
// Auto-refresh every 30 seconds
|
| 853 |
+
setInterval(refreshData, 30000);
|
| 854 |
+
</script>
|
| 855 |
+
</body>
|
| 856 |
+
</html>
|
| 857 |
+
|
archive_html/crypto_dashboard_pro.html
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Crypto Intelligence Console</title>
|
| 7 |
+
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body data-theme="dark">
|
| 11 |
+
<div class="app-shell">
|
| 12 |
+
<aside class="sidebar">
|
| 13 |
+
<div class="brand">
|
| 14 |
+
<strong>CRYPTO DT</strong>
|
| 15 |
+
<span class="env-pill">
|
| 16 |
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 17 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
|
| 18 |
+
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
|
| 19 |
+
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
|
| 20 |
+
</svg>
|
| 21 |
+
HF Space
|
| 22 |
+
</span>
|
| 23 |
+
</div>
|
| 24 |
+
<nav class="nav">
|
| 25 |
+
<button class="nav-button active" data-nav="page-overview">
|
| 26 |
+
<svg viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
| 27 |
+
Overview
|
| 28 |
+
</button>
|
| 29 |
+
<button class="nav-button" data-nav="page-market">
|
| 30 |
+
<svg viewBox="0 0 24 24"><path d="M3 17h2v-7H3v7zm4 0h2V7H7v10zm4 0h2V4h-2v13zm4 0h2V9h-2v8zm4 0h2V2h-2v15z"/></svg>
|
| 31 |
+
Market Intelligence
|
| 32 |
+
</button>
|
| 33 |
+
<button class="nav-button" data-nav="page-news">
|
| 34 |
+
<svg viewBox="0 0 24 24"><path d="M21 6h-4V4a2 2 0 0 0-2-2H5C3.897 2 3 2.897 3 4v14a2 2 0 0 0 2 2h13a3 3 0 0 0 3-3V6zm-6-2v14H5V4h10zm4 16a1 1 0 0 1-1 1h-1V8h2v12z"/></svg>
|
| 35 |
+
News & Sentiment
|
| 36 |
+
</button>
|
| 37 |
+
<button class="nav-button" data-nav="page-chart">
|
| 38 |
+
<svg viewBox="0 0 24 24"><path d="M5 3H3v18h18v-2H5z"/><path d="M7 15l3-3 4 4 6-6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
|
| 39 |
+
Chart Lab
|
| 40 |
+
</button>
|
| 41 |
+
<button class="nav-button" data-nav="page-ai">
|
| 42 |
+
<svg viewBox="0 0 24 24"><path d="M12 2a7 7 0 0 0-7 7c0 5.25 7 13 7 13s7-7.75 7-13a7 7 0 0 0-7-7zm0 9.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5z"/></svg>
|
| 43 |
+
AI Advisor
|
| 44 |
+
</button>
|
| 45 |
+
<button class="nav-button" data-nav="page-datasets">
|
| 46 |
+
<svg viewBox="0 0 24 24"><path d="M4 5v14l8 4 8-4V5l-8-4-8 4zm8-2.18L17.74 6 12 8.82 6.26 6 12 2.82zM6 8.97l6 2.82 6-2.82v3.06l-6 2.82-6-2.82V8.97zm0 5.03l6 2.82 6-2.82v3.2L12 20l-6-2.8v-3.2z"/></svg>
|
| 47 |
+
Datasets & Models
|
| 48 |
+
</button>
|
| 49 |
+
<button class="nav-button" data-nav="page-debug">
|
| 50 |
+
<svg viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm4 0h2v-2H7v2zm4 0h2v-2h-2v2zm4 0h2v-2h-2v2zm4-2v2h2v-2h-2z"/><path d="M5 5h14v4H5zm0 10h14v4H5z"/></svg>
|
| 51 |
+
System Health
|
| 52 |
+
</button>
|
| 53 |
+
<button class="nav-button" data-nav="page-settings">
|
| 54 |
+
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.61-.22l-2.39.96a7.03 7.03 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.5.42l-.36 2.54a7.03 7.03 0 0 0-1.63.94l-2.39-.96a.5.5 0 0 0-.61.22L2.69 8.53a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.5.5 0 0 0-.12.64l1.92 3.32c.14.24.43.34.68.22l2.39-.96c.5.4 1.05.72 1.63.94l.36 2.54c.04.26.25.42.5.42h3.8c.25 0 .46-.16.5-.42l.36-2.54c.58-.22 1.13-.54 1.63-.94l2.39.96c.25.12.54.02.68-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z"/></svg>
|
| 55 |
+
Settings
|
| 56 |
+
</button>
|
| 57 |
+
</nav>
|
| 58 |
+
<div class="sidebar-footer">
|
| 59 |
+
Unified crypto intelligence console<br />Designed for HF Spaces
|
| 60 |
+
</div>
|
| 61 |
+
</aside>
|
| 62 |
+
<main class="main-area">
|
| 63 |
+
<header class="topbar">
|
| 64 |
+
<div>
|
| 65 |
+
<h1>Professional Intelligence Dashboard</h1>
|
| 66 |
+
<p class="text-muted">Real-time analytics, AI insights, and provider telemetry</p>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="status-group">
|
| 69 |
+
<div class="status-pill" data-api-health data-state="warn">
|
| 70 |
+
<span class="status-dot"></span>
|
| 71 |
+
<span>checking</span>
|
| 72 |
+
</div>
|
| 73 |
+
<div class="status-pill" data-ws-status data-state="warn">
|
| 74 |
+
<span class="status-dot"></span>
|
| 75 |
+
<span>connecting</span>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</header>
|
| 79 |
+
<div class="page-container">
|
| 80 |
+
<section id="page-overview" class="page active">
|
| 81 |
+
<div class="section-header">
|
| 82 |
+
<h2 class="section-title">Global Overview</h2>
|
| 83 |
+
<span class="chip">Updated live from /api/market/stats</span>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="stats-grid" data-overview-stats></div>
|
| 86 |
+
<div class="grid-two">
|
| 87 |
+
<div class="glass-card">
|
| 88 |
+
<div class="section-header">
|
| 89 |
+
<h3>Top Coins</h3>
|
| 90 |
+
<span class="text-muted">Top performers by market cap</span>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="table-wrapper">
|
| 93 |
+
<table>
|
| 94 |
+
<thead>
|
| 95 |
+
<tr>
|
| 96 |
+
<th>#</th>
|
| 97 |
+
<th>Symbol</th>
|
| 98 |
+
<th>Name</th>
|
| 99 |
+
<th>Price</th>
|
| 100 |
+
<th>24h %</th>
|
| 101 |
+
<th>Volume</th>
|
| 102 |
+
<th>Market Cap</th>
|
| 103 |
+
</tr>
|
| 104 |
+
</thead>
|
| 105 |
+
<tbody data-top-coins-body></tbody>
|
| 106 |
+
</table>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="glass-card">
|
| 110 |
+
<div class="section-header">
|
| 111 |
+
<h3>Global Sentiment</h3>
|
| 112 |
+
<span class="text-muted">Powered by CryptoBERT stack</span>
|
| 113 |
+
</div>
|
| 114 |
+
<canvas id="sentiment-chart" height="220"></canvas>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</section>
|
| 118 |
+
|
| 119 |
+
<section id="page-market" class="page">
|
| 120 |
+
<div class="section-header">
|
| 121 |
+
<h2 class="section-title">Market Intelligence</h2>
|
| 122 |
+
<div class="controls-bar">
|
| 123 |
+
<div class="input-chip">
|
| 124 |
+
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M21 20l-5.6-5.6A6.5 6.5 0 1 0 15.4 16L21 21zM5 10.5a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0z" fill="currentColor"/></svg>
|
| 125 |
+
<input type="text" placeholder="Search symbol" data-market-search />
|
| 126 |
+
</div>
|
| 127 |
+
<div class="input-chip">
|
| 128 |
+
Timeframe:
|
| 129 |
+
<button class="ghost" data-timeframe="1d">1D</button>
|
| 130 |
+
<button class="ghost active" data-timeframe="7d">7D</button>
|
| 131 |
+
<button class="ghost" data-timeframe="30d">30D</button>
|
| 132 |
+
</div>
|
| 133 |
+
<label class="input-chip"> Live updates
|
| 134 |
+
<div class="toggle">
|
| 135 |
+
<input type="checkbox" data-live-toggle />
|
| 136 |
+
<span></span>
|
| 137 |
+
</div>
|
| 138 |
+
</label>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="glass-card">
|
| 142 |
+
<div class="table-wrapper">
|
| 143 |
+
<table>
|
| 144 |
+
<thead>
|
| 145 |
+
<tr>
|
| 146 |
+
<th>#</th>
|
| 147 |
+
<th>Symbol</th>
|
| 148 |
+
<th>Name</th>
|
| 149 |
+
<th>Price</th>
|
| 150 |
+
<th>24h %</th>
|
| 151 |
+
<th>Volume</th>
|
| 152 |
+
<th>Market Cap</th>
|
| 153 |
+
</tr>
|
| 154 |
+
</thead>
|
| 155 |
+
<tbody data-market-body></tbody>
|
| 156 |
+
</table>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
<div class="drawer" data-market-drawer>
|
| 160 |
+
<button class="ghost" data-close-drawer>Close</button>
|
| 161 |
+
<h3 data-drawer-symbol>—</h3>
|
| 162 |
+
<div data-drawer-stats></div>
|
| 163 |
+
<div class="glass-card" data-chart-wrapper>
|
| 164 |
+
<canvas id="market-detail-chart" height="180"></canvas>
|
| 165 |
+
</div>
|
| 166 |
+
<div class="glass-card">
|
| 167 |
+
<h4>Latest Headlines</h4>
|
| 168 |
+
<div data-drawer-news></div>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
</section>
|
| 172 |
+
|
| 173 |
+
<section id="page-news" class="page">
|
| 174 |
+
<div class="section-header">
|
| 175 |
+
<h2 class="section-title">News & Sentiment</h2>
|
| 176 |
+
</div>
|
| 177 |
+
<div class="controls-bar">
|
| 178 |
+
<select data-news-range>
|
| 179 |
+
<option value="24h">Last 24h</option>
|
| 180 |
+
<option value="7d">7 Days</option>
|
| 181 |
+
<option value="30d">30 Days</option>
|
| 182 |
+
</select>
|
| 183 |
+
<input type="text" placeholder="Search headline" data-news-search />
|
| 184 |
+
<input type="text" placeholder="Filter symbol (e.g. BTC)" data-news-symbol />
|
| 185 |
+
</div>
|
| 186 |
+
<div class="glass-card">
|
| 187 |
+
<div class="table-wrapper">
|
| 188 |
+
<table>
|
| 189 |
+
<thead>
|
| 190 |
+
<tr>
|
| 191 |
+
<th>Time</th>
|
| 192 |
+
<th>Source</th>
|
| 193 |
+
<th>Title</th>
|
| 194 |
+
<th>Symbols</th>
|
| 195 |
+
<th>Sentiment</th>
|
| 196 |
+
<th>Impact</th>
|
| 197 |
+
</tr>
|
| 198 |
+
</thead>
|
| 199 |
+
<tbody data-news-body></tbody>
|
| 200 |
+
</table>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
<div class="modal-backdrop" data-news-modal>
|
| 204 |
+
<div class="modal">
|
| 205 |
+
<button class="ghost" data-close-news-modal>Close</button>
|
| 206 |
+
<div data-news-modal-content></div>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</section>
|
| 210 |
+
|
| 211 |
+
<section id="page-chart" class="page">
|
| 212 |
+
<div class="section-header">
|
| 213 |
+
<h2 class="section-title">Chart & Pattern Analysis</h2>
|
| 214 |
+
<div class="controls-bar">
|
| 215 |
+
<select data-chart-symbol>
|
| 216 |
+
<option value="BTC">BTC</option>
|
| 217 |
+
<option value="ETH">ETH</option>
|
| 218 |
+
<option value="SOL">SOL</option>
|
| 219 |
+
</select>
|
| 220 |
+
<div>
|
| 221 |
+
<button class="ghost active" data-chart-timeframe="7d">7D</button>
|
| 222 |
+
<button class="ghost" data-chart-timeframe="30d">30D</button>
|
| 223 |
+
<button class="ghost" data-chart-timeframe="90d">90D</button>
|
| 224 |
+
</div>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="glass-card">
|
| 228 |
+
<canvas id="chart-lab-canvas" height="240"></canvas>
|
| 229 |
+
</div>
|
| 230 |
+
<div class="glass-card">
|
| 231 |
+
<h4>Indicators</h4>
|
| 232 |
+
<div class="controls-bar">
|
| 233 |
+
<label><input type="checkbox" data-indicator value="MA20" checked /> MA 20</label>
|
| 234 |
+
<label><input type="checkbox" data-indicator value="MA50" /> MA 50</label>
|
| 235 |
+
<label><input type="checkbox" data-indicator value="RSI" /> RSI</label>
|
| 236 |
+
<label><input type="checkbox" data-indicator value="Volume" /> Volume</label>
|
| 237 |
+
</div>
|
| 238 |
+
<button class="primary" data-run-analysis>Run AI Analysis</button>
|
| 239 |
+
<div data-ai-insights class="ai-insights"></div>
|
| 240 |
+
</div>
|
| 241 |
+
</section>
|
| 242 |
+
|
| 243 |
+
<section id="page-ai" class="page">
|
| 244 |
+
<div class="section-header">
|
| 245 |
+
<h2 class="section-title">AI Trade Advisor</h2>
|
| 246 |
+
</div>
|
| 247 |
+
<div class="glass-card">
|
| 248 |
+
<form data-ai-form class="ai-form">
|
| 249 |
+
<div class="grid-two">
|
| 250 |
+
<label>Symbol
|
| 251 |
+
<select name="symbol">
|
| 252 |
+
<option value="BTC">BTC</option>
|
| 253 |
+
<option value="ETH">ETH</option>
|
| 254 |
+
<option value="SOL">SOL</option>
|
| 255 |
+
</select>
|
| 256 |
+
</label>
|
| 257 |
+
<label>Time Horizon
|
| 258 |
+
<select name="horizon">
|
| 259 |
+
<option value="intraday">Intraday</option>
|
| 260 |
+
<option value="swing">Swing</option>
|
| 261 |
+
<option value="long">Long Term</option>
|
| 262 |
+
</select>
|
| 263 |
+
</label>
|
| 264 |
+
<label>Risk Profile
|
| 265 |
+
<select name="risk">
|
| 266 |
+
<option value="conservative">Conservative</option>
|
| 267 |
+
<option value="moderate">Moderate</option>
|
| 268 |
+
<option value="aggressive">Aggressive</option>
|
| 269 |
+
</select>
|
| 270 |
+
</label>
|
| 271 |
+
<label>Context
|
| 272 |
+
<textarea name="context" placeholder="Optional prompts for the advisor"></textarea>
|
| 273 |
+
</label>
|
| 274 |
+
</div>
|
| 275 |
+
<button class="primary" type="submit">Generate AI Advice</button>
|
| 276 |
+
</form>
|
| 277 |
+
<div data-ai-result class="ai-result"></div>
|
| 278 |
+
<div class="inline-message inline-info" data-ai-disclaimer>
|
| 279 |
+
This is experimental AI research, not financial advice.
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
</section>
|
| 283 |
+
|
| 284 |
+
<section id="page-datasets" class="page">
|
| 285 |
+
<div class="section-header">
|
| 286 |
+
<h2 class="section-title">Datasets & Models Lab</h2>
|
| 287 |
+
</div>
|
| 288 |
+
<div class="grid-two">
|
| 289 |
+
<div class="glass-card">
|
| 290 |
+
<h3>Datasets</h3>
|
| 291 |
+
<div class="table-wrapper">
|
| 292 |
+
<table>
|
| 293 |
+
<thead>
|
| 294 |
+
<tr>
|
| 295 |
+
<th>Name</th>
|
| 296 |
+
<th>Type</th>
|
| 297 |
+
<th>Updated</th>
|
| 298 |
+
<th>Preview</th>
|
| 299 |
+
</tr>
|
| 300 |
+
</thead>
|
| 301 |
+
<tbody data-datasets-body></tbody>
|
| 302 |
+
</table>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
<div class="glass-card">
|
| 306 |
+
<h3>Models</h3>
|
| 307 |
+
<div class="table-wrapper">
|
| 308 |
+
<table>
|
| 309 |
+
<thead>
|
| 310 |
+
<tr>
|
| 311 |
+
<th>Name</th>
|
| 312 |
+
<th>Task</th>
|
| 313 |
+
<th>Status</th>
|
| 314 |
+
<th>Notes</th>
|
| 315 |
+
</tr>
|
| 316 |
+
</thead>
|
| 317 |
+
<tbody data-models-body></tbody>
|
| 318 |
+
</table>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
<div class="glass-card">
|
| 323 |
+
<h4>Test a Model</h4>
|
| 324 |
+
<form data-model-test-form class="grid-two">
|
| 325 |
+
<label>Model
|
| 326 |
+
<select data-model-select name="model"></select>
|
| 327 |
+
</label>
|
| 328 |
+
<label>Input
|
| 329 |
+
<textarea name="input" placeholder="Type a prompt"></textarea>
|
| 330 |
+
</label>
|
| 331 |
+
<button class="primary" type="submit">Run Test</button>
|
| 332 |
+
</form>
|
| 333 |
+
<div data-model-test-output></div>
|
| 334 |
+
</div>
|
| 335 |
+
<div class="modal-backdrop" data-dataset-modal>
|
| 336 |
+
<div class="modal">
|
| 337 |
+
<button class="ghost" data-close-dataset-modal>Close</button>
|
| 338 |
+
<div data-dataset-modal-content></div>
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
</section>
|
| 342 |
+
|
| 343 |
+
<section id="page-debug" class="page">
|
| 344 |
+
<div class="section-header">
|
| 345 |
+
<h2 class="section-title">System Health & Debug Console</h2>
|
| 346 |
+
<button class="ghost" data-refresh-health>Refresh</button>
|
| 347 |
+
</div>
|
| 348 |
+
<div class="stats-grid">
|
| 349 |
+
<div class="glass-card">
|
| 350 |
+
<h3>API Health</h3>
|
| 351 |
+
<div class="stat-value" data-health-status>—</div>
|
| 352 |
+
</div>
|
| 353 |
+
<div class="glass-card">
|
| 354 |
+
<h3>Providers</h3>
|
| 355 |
+
<div data-providers class="grid-two"></div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
<div class="grid-two">
|
| 359 |
+
<div class="glass-card">
|
| 360 |
+
<h4>Request Log</h4>
|
| 361 |
+
<div class="table-wrapper log-table">
|
| 362 |
+
<table>
|
| 363 |
+
<thead>
|
| 364 |
+
<tr>
|
| 365 |
+
<th>Time</th>
|
| 366 |
+
<th>Method</th>
|
| 367 |
+
<th>Endpoint</th>
|
| 368 |
+
<th>Status</th>
|
| 369 |
+
<th>Latency</th>
|
| 370 |
+
</tr>
|
| 371 |
+
</thead>
|
| 372 |
+
<tbody data-request-log></tbody>
|
| 373 |
+
</table>
|
| 374 |
+
</div>
|
| 375 |
+
</div>
|
| 376 |
+
<div class="glass-card">
|
| 377 |
+
<h4>Error Log</h4>
|
| 378 |
+
<div class="table-wrapper log-table">
|
| 379 |
+
<table>
|
| 380 |
+
<thead>
|
| 381 |
+
<tr>
|
| 382 |
+
<th>Time</th>
|
| 383 |
+
<th>Endpoint</th>
|
| 384 |
+
<th>Message</th>
|
| 385 |
+
</tr>
|
| 386 |
+
</thead>
|
| 387 |
+
<tbody data-error-log></tbody>
|
| 388 |
+
</table>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
</div>
|
| 392 |
+
<div class="glass-card">
|
| 393 |
+
<h4>WebSocket Events</h4>
|
| 394 |
+
<div class="table-wrapper log-table">
|
| 395 |
+
<table>
|
| 396 |
+
<thead>
|
| 397 |
+
<tr>
|
| 398 |
+
<th>Time</th>
|
| 399 |
+
<th>Type</th>
|
| 400 |
+
<th>Detail</th>
|
| 401 |
+
</tr>
|
| 402 |
+
</thead>
|
| 403 |
+
<tbody data-ws-log></tbody>
|
| 404 |
+
</table>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
</section>
|
| 408 |
+
|
| 409 |
+
<section id="page-settings" class="page">
|
| 410 |
+
<div class="section-header">
|
| 411 |
+
<h2 class="section-title">Settings</h2>
|
| 412 |
+
</div>
|
| 413 |
+
<div class="glass-card">
|
| 414 |
+
<div class="grid-two">
|
| 415 |
+
<label class="input-chip">Light Theme
|
| 416 |
+
<div class="toggle">
|
| 417 |
+
<input type="checkbox" data-theme-toggle />
|
| 418 |
+
<span></span>
|
| 419 |
+
</div>
|
| 420 |
+
</label>
|
| 421 |
+
<label>Market Refresh (sec)
|
| 422 |
+
<input type="number" min="15" step="5" data-market-interval />
|
| 423 |
+
</label>
|
| 424 |
+
<label>News Refresh (sec)
|
| 425 |
+
<input type="number" min="30" step="10" data-news-interval />
|
| 426 |
+
</label>
|
| 427 |
+
<label class="input-chip">Compact Layout
|
| 428 |
+
<div class="toggle">
|
| 429 |
+
<input type="checkbox" data-layout-toggle />
|
| 430 |
+
<span></span>
|
| 431 |
+
</div>
|
| 432 |
+
</label>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
</section>
|
| 436 |
+
</div>
|
| 437 |
+
</main>
|
| 438 |
+
</div>
|
| 439 |
+
<script type="module" src="static/js/app.js"></script>
|
| 440 |
+
</body>
|
| 441 |
+
</html>
|
archive_html/dashboard.html
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Crypto Intelligence Dashboard</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
| 10 |
+
<link rel="stylesheet" href="/static/css/unified-ui.css" />
|
| 11 |
+
<link rel="stylesheet" href="/static/css/components.css" />
|
| 12 |
+
<script defer src="/static/js/ui-feedback.js"></script>
|
| 13 |
+
<script defer src="/static/js/dashboard-app.js"></script>
|
| 14 |
+
</head>
|
| 15 |
+
<body class="page page-dashboard">
|
| 16 |
+
<header class="top-nav">
|
| 17 |
+
<div class="branding">
|
| 18 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
|
| 19 |
+
<div>
|
| 20 |
+
<strong>Crypto Intelligence Hub</strong>
|
| 21 |
+
<small style="color:var(--ui-text-muted);letter-spacing:0.2em;">Real-time data + HF models</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<nav class="nav-links">
|
| 25 |
+
<a class="active" href="/dashboard">Dashboard</a>
|
| 26 |
+
<a href="/admin">Admin</a>
|
| 27 |
+
<a href="/hf_console">HF Console</a>
|
| 28 |
+
<a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
|
| 29 |
+
</nav>
|
| 30 |
+
</header>
|
| 31 |
+
|
| 32 |
+
<main class="page-content">
|
| 33 |
+
<section class="card" id="intro-card">
|
| 34 |
+
<div class="section-heading">
|
| 35 |
+
<h2>Unified Market Pulse</h2>
|
| 36 |
+
<span class="badge info" id="intro-source">Loading...</span>
|
| 37 |
+
</div>
|
| 38 |
+
<p style="color:var(--text-muted);max-width:780px;line-height:1.6;">
|
| 39 |
+
Live collectors + local fallback registry guarantee resilient insights. All numbers below already honor the FastAPI routes
|
| 40 |
+
(<code>/api/crypto/prices/top</code>, <code>/api/crypto/market-overview</code>, <code>/health</code>) so you can monitor status even when providers degrade.
|
| 41 |
+
</p>
|
| 42 |
+
</section>
|
| 43 |
+
|
| 44 |
+
<section class="card-grid" id="market-metrics">
|
| 45 |
+
<article class="card"><h3>Total Market Cap</h3><div class="metric-value" id="metric-market-cap">-</div><div class="metric-subtext" id="metric-cap-source"></div></article>
|
| 46 |
+
<article class="card"><h3>24h Volume</h3><div class="metric-value" id="metric-volume">-</div><div class="metric-subtext" id="metric-volume-source"></div></article>
|
| 47 |
+
<article class="card"><h3>BTC Dominance</h3><div class="metric-value" id="metric-btc-dom">-</div><div class="metric-subtext">Based on /api/crypto/market-overview</div></article>
|
| 48 |
+
<article class="card"><h3>System Health</h3><div class="metric-value" id="metric-health">-</div><div class="metric-subtext" id="metric-health-details"></div></article>
|
| 49 |
+
</section>
|
| 50 |
+
|
| 51 |
+
<section class="card table-card">
|
| 52 |
+
<div class="section-heading">
|
| 53 |
+
<h2>Top Assets</h2>
|
| 54 |
+
<span class="badge info" id="top-prices-source">Loading...</span>
|
| 55 |
+
</div>
|
| 56 |
+
<div class="table-wrapper">
|
| 57 |
+
<table>
|
| 58 |
+
<thead>
|
| 59 |
+
<tr><th>Symbol</th><th>Price</th><th>24h %</th><th>Volume</th></tr>
|
| 60 |
+
</thead>
|
| 61 |
+
<tbody id="top-prices-table">
|
| 62 |
+
<tr><td colspan="4">Loading...</td></tr>
|
| 63 |
+
</tbody>
|
| 64 |
+
</table>
|
| 65 |
+
</div>
|
| 66 |
+
</section>
|
| 67 |
+
|
| 68 |
+
<section class="split-grid">
|
| 69 |
+
<article class="card" id="overview-card">
|
| 70 |
+
<div class="section-heading">
|
| 71 |
+
<h2>Market Overview</h2>
|
| 72 |
+
<span class="badge info" id="market-overview-source">Loading...</span>
|
| 73 |
+
</div>
|
| 74 |
+
<ul class="list" id="market-overview-list"></ul>
|
| 75 |
+
</article>
|
| 76 |
+
<article class="card" id="system-card">
|
| 77 |
+
<div class="section-heading">
|
| 78 |
+
<h2>System & Rate Limits</h2>
|
| 79 |
+
<span class="badge info" id="system-status-source">/health</span>
|
| 80 |
+
</div>
|
| 81 |
+
<div id="system-health-status" class="metric-subtext"></div>
|
| 82 |
+
<ul class="list" id="system-status-list"></ul>
|
| 83 |
+
<div class="section-heading" style="margin-top:24px;">
|
| 84 |
+
<h2>Configuration</h2>
|
| 85 |
+
</div>
|
| 86 |
+
<ul class="list" id="system-config-list"></ul>
|
| 87 |
+
<div class="section-heading" style="margin-top:24px;">
|
| 88 |
+
<h2>Rate Limits</h2>
|
| 89 |
+
</div>
|
| 90 |
+
<ul class="list" id="rate-limits-list"></ul>
|
| 91 |
+
</article>
|
| 92 |
+
</section>
|
| 93 |
+
|
| 94 |
+
<section class="split-grid">
|
| 95 |
+
<article class="card" id="hf-widget">
|
| 96 |
+
<div class="section-heading">
|
| 97 |
+
<h2>HuggingFace Snapshot</h2>
|
| 98 |
+
<span class="badge info" id="hf-health-status">Loading...</span>
|
| 99 |
+
</div>
|
| 100 |
+
<div id="hf-widget-summary" class="metric-subtext"></div>
|
| 101 |
+
<ul class="list" id="hf-registry-list"></ul>
|
| 102 |
+
</article>
|
| 103 |
+
<article class="card">
|
| 104 |
+
<div class="section-heading">
|
| 105 |
+
<h2>Live Stream (/ws)</h2>
|
| 106 |
+
<span class="badge info" id="ws-status">Connecting...</span>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="ws-stream" id="ws-stream"></div>
|
| 109 |
+
</article>
|
| 110 |
+
</section>
|
| 111 |
+
</main>
|
| 112 |
+
</body>
|
| 113 |
+
</html>
|
archive_html/dashboard_standalone.html
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto Monitor - Provider Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
:root {
|
| 15 |
+
--primary: #6366f1;
|
| 16 |
+
--primary-dark: #4f46e5;
|
| 17 |
+
--success: #10b981;
|
| 18 |
+
--warning: #f59e0b;
|
| 19 |
+
--danger: #ef4444;
|
| 20 |
+
--info: #3b82f6;
|
| 21 |
+
--bg-dark: #0f172a;
|
| 22 |
+
--bg-card: #1e293b;
|
| 23 |
+
--bg-hover: #334155;
|
| 24 |
+
--text-light: #f1f5f9;
|
| 25 |
+
--text-muted: #94a3b8;
|
| 26 |
+
--border: #334155;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 31 |
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
| 32 |
+
color: var(--text-light);
|
| 33 |
+
line-height: 1.6;
|
| 34 |
+
min-height: 100vh;
|
| 35 |
+
padding: 20px;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.container {
|
| 39 |
+
max-width: 1600px;
|
| 40 |
+
margin: 0 auto;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
header {
|
| 44 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
| 45 |
+
padding: 30px;
|
| 46 |
+
border-radius: 16px;
|
| 47 |
+
margin-bottom: 30px;
|
| 48 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
| 49 |
+
text-align: center;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
header h1 {
|
| 53 |
+
font-size: 32px;
|
| 54 |
+
font-weight: 700;
|
| 55 |
+
margin-bottom: 8px;
|
| 56 |
+
display: flex;
|
| 57 |
+
align-items: center;
|
| 58 |
+
justify-content: center;
|
| 59 |
+
gap: 12px;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.stats-grid {
|
| 63 |
+
display: grid;
|
| 64 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 65 |
+
gap: 20px;
|
| 66 |
+
margin-bottom: 30px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.stat-card {
|
| 70 |
+
background: var(--bg-card);
|
| 71 |
+
padding: 24px;
|
| 72 |
+
border-radius: 12px;
|
| 73 |
+
border: 1px solid var(--border);
|
| 74 |
+
border-top: 4px solid var(--primary);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.stat-card:hover {
|
| 78 |
+
transform: translateY(-4px);
|
| 79 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
| 80 |
+
transition: all 0.3s;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.stat-card .label {
|
| 84 |
+
color: var(--text-muted);
|
| 85 |
+
font-size: 13px;
|
| 86 |
+
text-transform: uppercase;
|
| 87 |
+
letter-spacing: 0.5px;
|
| 88 |
+
font-weight: 600;
|
| 89 |
+
margin-bottom: 8px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.stat-card .value {
|
| 93 |
+
font-size: 36px;
|
| 94 |
+
font-weight: 700;
|
| 95 |
+
margin: 8px 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.filters {
|
| 99 |
+
background: var(--bg-card);
|
| 100 |
+
padding: 20px;
|
| 101 |
+
border-radius: 12px;
|
| 102 |
+
margin-bottom: 20px;
|
| 103 |
+
border: 1px solid var(--border);
|
| 104 |
+
display: flex;
|
| 105 |
+
gap: 15px;
|
| 106 |
+
flex-wrap: wrap;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.search-box input, .filters select, .filters button {
|
| 110 |
+
padding: 10px 16px;
|
| 111 |
+
background: var(--bg-dark);
|
| 112 |
+
border: 1px solid var(--border);
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
color: var(--text-light);
|
| 115 |
+
font-size: 14px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.filters button {
|
| 119 |
+
background: var(--primary);
|
| 120 |
+
border-color: var(--primary);
|
| 121 |
+
cursor: pointer;
|
| 122 |
+
font-weight: 600;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.filters button:hover {
|
| 126 |
+
background: var(--primary-dark);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.table-container {
|
| 130 |
+
background: var(--bg-card);
|
| 131 |
+
border-radius: 12px;
|
| 132 |
+
border: 1px solid var(--border);
|
| 133 |
+
overflow: hidden;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
table {
|
| 137 |
+
width: 100%;
|
| 138 |
+
border-collapse: collapse;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
thead {
|
| 142 |
+
background: var(--bg-dark);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
thead th {
|
| 146 |
+
text-align: left;
|
| 147 |
+
padding: 16px 20px;
|
| 148 |
+
font-weight: 600;
|
| 149 |
+
font-size: 13px;
|
| 150 |
+
text-transform: uppercase;
|
| 151 |
+
color: var(--text-muted);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
tbody tr {
|
| 155 |
+
border-bottom: 1px solid var(--border);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
tbody tr:hover {
|
| 159 |
+
background: var(--bg-hover);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
tbody td {
|
| 163 |
+
padding: 16px 20px;
|
| 164 |
+
font-size: 14px;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.badge {
|
| 168 |
+
display: inline-block;
|
| 169 |
+
padding: 6px 12px;
|
| 170 |
+
border-radius: 20px;
|
| 171 |
+
font-size: 12px;
|
| 172 |
+
font-weight: 600;
|
| 173 |
+
text-transform: uppercase;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.badge.validated {
|
| 177 |
+
background: rgba(16, 185, 129, 0.15);
|
| 178 |
+
color: var(--success);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.badge.unvalidated {
|
| 182 |
+
background: rgba(239, 68, 68, 0.15);
|
| 183 |
+
color: var(--danger);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.category {
|
| 187 |
+
padding: 4px 10px;
|
| 188 |
+
border-radius: 6px;
|
| 189 |
+
font-size: 12px;
|
| 190 |
+
background: rgba(99, 102, 241, 0.1);
|
| 191 |
+
color: var(--primary);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.type {
|
| 195 |
+
padding: 4px 8px;
|
| 196 |
+
border-radius: 4px;
|
| 197 |
+
font-size: 11px;
|
| 198 |
+
background: rgba(59, 130, 246, 0.1);
|
| 199 |
+
color: var(--info);
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.response-time {
|
| 203 |
+
font-weight: 600;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.response-time.fast { color: var(--success); }
|
| 207 |
+
.response-time.medium { color: var(--warning); }
|
| 208 |
+
.response-time.slow { color: var(--danger); }
|
| 209 |
+
|
| 210 |
+
@media (max-width: 768px) {
|
| 211 |
+
.stats-grid {
|
| 212 |
+
grid-template-columns: 1fr;
|
| 213 |
+
}
|
| 214 |
+
.filters {
|
| 215 |
+
flex-direction: column;
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
</style>
|
| 219 |
+
</head>
|
| 220 |
+
<body>
|
| 221 |
+
<div class="container">
|
| 222 |
+
<header>
|
| 223 |
+
<h1>
|
| 224 |
+
<span style="font-size: 40px;">📊</span>
|
| 225 |
+
Crypto Provider Monitor
|
| 226 |
+
</h1>
|
| 227 |
+
<p style="color: rgba(255, 255, 255, 0.85);">Real-time API Provider Monitoring Dashboard</p>
|
| 228 |
+
</header>
|
| 229 |
+
|
| 230 |
+
<div class="stats-grid">
|
| 231 |
+
<div class="stat-card">
|
| 232 |
+
<div class="label">Total Providers</div>
|
| 233 |
+
<div class="value" style="color: var(--primary);" id="totalProviders">-</div>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="stat-card">
|
| 236 |
+
<div class="label">✅ Validated</div>
|
| 237 |
+
<div class="value" style="color: var(--success);" id="validatedCount">-</div>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="stat-card">
|
| 240 |
+
<div class="label">❌ Unvalidated</div>
|
| 241 |
+
<div class="value" style="color: var(--danger);" id="unvalidatedCount">-</div>
|
| 242 |
+
</div>
|
| 243 |
+
<div class="stat-card">
|
| 244 |
+
<div class="label">⚡ Avg Response</div>
|
| 245 |
+
<div class="value" style="color: var(--warning); font-size: 28px;" id="avgResponse">- ms</div>
|
| 246 |
+
</div>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
<div class="filters">
|
| 250 |
+
<select id="categoryFilter" onchange="applyFilters()">
|
| 251 |
+
<option value="all">All Categories</option>
|
| 252 |
+
</select>
|
| 253 |
+
<select id="statusFilter" onchange="applyFilters()">
|
| 254 |
+
<option value="all">All Status</option>
|
| 255 |
+
<option value="validated">Validated</option>
|
| 256 |
+
<option value="unvalidated">Unvalidated</option>
|
| 257 |
+
</select>
|
| 258 |
+
<div class="search-box" style="flex: 1; min-width: 250px;">
|
| 259 |
+
<input type="text" id="searchInput" placeholder="Search providers..." onkeyup="applyFilters()">
|
| 260 |
+
</div>
|
| 261 |
+
<button onclick="refreshData()">🔄 Refresh</button>
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
<div class="table-container">
|
| 265 |
+
<table>
|
| 266 |
+
<thead>
|
| 267 |
+
<tr>
|
| 268 |
+
<th>Provider ID</th>
|
| 269 |
+
<th>Name</th>
|
| 270 |
+
<th>Category</th>
|
| 271 |
+
<th>Type</th>
|
| 272 |
+
<th>Status</th>
|
| 273 |
+
<th>Response Time</th>
|
| 274 |
+
</tr>
|
| 275 |
+
</thead>
|
| 276 |
+
<tbody id="providersTable">
|
| 277 |
+
<tr><td colspan="6" style="text-align: center; padding: 40px;">Loading...</td></tr>
|
| 278 |
+
</tbody>
|
| 279 |
+
</table>
|
| 280 |
+
</div>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<script>
|
| 284 |
+
let allProviders = [];
|
| 285 |
+
|
| 286 |
+
// Auto-detect API endpoint
|
| 287 |
+
function getAPIEndpoint() {
|
| 288 |
+
const host = window.location.hostname;
|
| 289 |
+
const port = window.location.port;
|
| 290 |
+
const protocol = window.location.protocol;
|
| 291 |
+
|
| 292 |
+
// For Hugging Face Spaces
|
| 293 |
+
if (host.includes('hf.space') || host.includes('huggingface')) {
|
| 294 |
+
return `${protocol}//${host}/api/providers`;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// For local development
|
| 298 |
+
if (port) {
|
| 299 |
+
return `${protocol}//${host}:${port}/api/providers`;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
return '/api/providers';
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
async function fetchProviders() {
|
| 306 |
+
try {
|
| 307 |
+
const response = await fetch(getAPIEndpoint());
|
| 308 |
+
const data = await response.json();
|
| 309 |
+
allProviders = data.providers || [];
|
| 310 |
+
updateUI();
|
| 311 |
+
} catch (error) {
|
| 312 |
+
console.error('Error:', error);
|
| 313 |
+
document.getElementById('providersTable').innerHTML =
|
| 314 |
+
'<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--danger);">Error loading providers</td></tr>';
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
function updateUI() {
|
| 319 |
+
updateStats();
|
| 320 |
+
populateFilters();
|
| 321 |
+
applyFilters();
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
function updateStats() {
|
| 325 |
+
const total = allProviders.length;
|
| 326 |
+
const validated = allProviders.filter(p => p.status === 'validated').length;
|
| 327 |
+
const unvalidated = total - validated;
|
| 328 |
+
|
| 329 |
+
const responseTimes = allProviders
|
| 330 |
+
.filter(p => p.response_time_ms > 0)
|
| 331 |
+
.map(p => p.response_time_ms);
|
| 332 |
+
const avgResponse = responseTimes.length > 0
|
| 333 |
+
? Math.round(responseTimes.reduce((a, b) => a + b) / responseTimes.length)
|
| 334 |
+
: 0;
|
| 335 |
+
|
| 336 |
+
document.getElementById('totalProviders').textContent = total;
|
| 337 |
+
document.getElementById('validatedCount').textContent = validated;
|
| 338 |
+
document.getElementById('unvalidatedCount').textContent = unvalidated;
|
| 339 |
+
document.getElementById('avgResponse').textContent = avgResponse > 0 ? `${avgResponse} ms` : 'N/A';
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function populateFilters() {
|
| 343 |
+
const categories = [...new Set(allProviders.map(p => p.category))].filter(c => c).sort();
|
| 344 |
+
const categoryFilter = document.getElementById('categoryFilter');
|
| 345 |
+
categoryFilter.innerHTML = '<option value="all">All Categories</option>';
|
| 346 |
+
categories.forEach(cat => {
|
| 347 |
+
categoryFilter.innerHTML += `<option value="${cat}">${cat.replace(/_/g, ' ').toUpperCase()}</option>`;
|
| 348 |
+
});
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
function applyFilters() {
|
| 352 |
+
const category = document.getElementById('categoryFilter').value;
|
| 353 |
+
const status = document.getElementById('statusFilter').value;
|
| 354 |
+
const search = document.getElementById('searchInput').value.toLowerCase();
|
| 355 |
+
|
| 356 |
+
let filtered = allProviders.filter(p => {
|
| 357 |
+
const matchCategory = category === 'all' || p.category === category;
|
| 358 |
+
const matchStatus = status === 'all' || p.status === status;
|
| 359 |
+
const matchSearch = !search ||
|
| 360 |
+
p.name.toLowerCase().includes(search) ||
|
| 361 |
+
p.provider_id.toLowerCase().includes(search) ||
|
| 362 |
+
(p.category && p.category.toLowerCase().includes(search));
|
| 363 |
+
return matchCategory && matchStatus && matchSearch;
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
renderTable(filtered);
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
function renderTable(providers) {
|
| 370 |
+
const tbody = document.getElementById('providersTable');
|
| 371 |
+
|
| 372 |
+
if (providers.length === 0) {
|
| 373 |
+
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px;">No providers found</td></tr>';
|
| 374 |
+
return;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
tbody.innerHTML = providers.map(p => {
|
| 378 |
+
const responseTime = p.response_time_ms || 0;
|
| 379 |
+
let responseClass = 'fast';
|
| 380 |
+
if (responseTime > 500) responseClass = 'slow';
|
| 381 |
+
else if (responseTime > 200) responseClass = 'medium';
|
| 382 |
+
|
| 383 |
+
return `
|
| 384 |
+
<tr>
|
| 385 |
+
<td><code style="color: var(--text-muted); font-size: 12px;">${p.provider_id}</code></td>
|
| 386 |
+
<td><strong>${p.name}</strong></td>
|
| 387 |
+
<td><span class="category">${(p.category || 'unknown').replace(/_/g, ' ')}</span></td>
|
| 388 |
+
<td><span class="type">${(p.type || 'unknown').replace(/_/g, ' ')}</span></td>
|
| 389 |
+
<td><span class="badge ${p.status}">${p.status}</span></td>
|
| 390 |
+
<td>${responseTime > 0 ?
|
| 391 |
+
`<span class="response-time ${responseClass}">${Math.round(responseTime)} ms</span>` :
|
| 392 |
+
'<span style="color: var(--text-muted);">N/A</span>'
|
| 393 |
+
}</td>
|
| 394 |
+
</tr>
|
| 395 |
+
`;
|
| 396 |
+
}).join('');
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
function refreshData() {
|
| 400 |
+
fetchProviders();
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
// Initial load
|
| 404 |
+
fetchProviders();
|
| 405 |
+
|
| 406 |
+
// Auto-refresh every 30 seconds
|
| 407 |
+
setInterval(fetchProviders, 30000);
|
| 408 |
+
</script>
|
| 409 |
+
</body>
|
| 410 |
+
</html>
|
archive_html/enhanced_dashboard.html
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Enhanced Crypto Data Tracker</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
padding: 20px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 1600px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
header {
|
| 27 |
+
background: rgba(255, 255, 255, 0.1);
|
| 28 |
+
backdrop-filter: blur(10px);
|
| 29 |
+
border-radius: 15px;
|
| 30 |
+
padding: 20px 30px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
display: flex;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: center;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1 {
|
| 38 |
+
color: white;
|
| 39 |
+
font-size: 28px;
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
gap: 10px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.connection-status {
|
| 46 |
+
display: flex;
|
| 47 |
+
align-items: center;
|
| 48 |
+
gap: 10px;
|
| 49 |
+
color: white;
|
| 50 |
+
font-size: 14px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.status-indicator {
|
| 54 |
+
width: 10px;
|
| 55 |
+
height: 10px;
|
| 56 |
+
border-radius: 50%;
|
| 57 |
+
background: #10b981;
|
| 58 |
+
animation: pulse 2s infinite;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.status-indicator.disconnected {
|
| 62 |
+
background: #ef4444;
|
| 63 |
+
animation: none;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
@keyframes pulse {
|
| 67 |
+
0%, 100% { opacity: 1; }
|
| 68 |
+
50% { opacity: 0.5; }
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.controls {
|
| 72 |
+
background: rgba(255, 255, 255, 0.1);
|
| 73 |
+
backdrop-filter: blur(10px);
|
| 74 |
+
border-radius: 15px;
|
| 75 |
+
padding: 20px;
|
| 76 |
+
margin-bottom: 20px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.controls-row {
|
| 80 |
+
display: flex;
|
| 81 |
+
gap: 15px;
|
| 82 |
+
flex-wrap: wrap;
|
| 83 |
+
align-items: center;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.btn {
|
| 87 |
+
padding: 10px 20px;
|
| 88 |
+
border: none;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
font-size: 14px;
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
transition: all 0.3s ease;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
gap: 8px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.btn-primary {
|
| 100 |
+
background: white;
|
| 101 |
+
color: #667eea;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.btn-primary:hover {
|
| 105 |
+
transform: translateY(-2px);
|
| 106 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.btn-success {
|
| 110 |
+
background: #10b981;
|
| 111 |
+
color: white;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.btn-danger {
|
| 115 |
+
background: #ef4444;
|
| 116 |
+
color: white;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.btn-info {
|
| 120 |
+
background: #3b82f6;
|
| 121 |
+
color: white;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.btn:disabled {
|
| 125 |
+
opacity: 0.5;
|
| 126 |
+
cursor: not-allowed;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.grid {
|
| 130 |
+
display: grid;
|
| 131 |
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
| 132 |
+
gap: 20px;
|
| 133 |
+
margin-bottom: 20px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.card {
|
| 137 |
+
background: rgba(255, 255, 255, 0.1);
|
| 138 |
+
backdrop-filter: blur(10px);
|
| 139 |
+
border-radius: 15px;
|
| 140 |
+
padding: 20px;
|
| 141 |
+
color: white;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.card h2 {
|
| 145 |
+
font-size: 18px;
|
| 146 |
+
margin-bottom: 15px;
|
| 147 |
+
display: flex;
|
| 148 |
+
align-items: center;
|
| 149 |
+
gap: 10px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.stat-grid {
|
| 153 |
+
display: grid;
|
| 154 |
+
grid-template-columns: repeat(2, 1fr);
|
| 155 |
+
gap: 15px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.stat-item {
|
| 159 |
+
background: rgba(255, 255, 255, 0.1);
|
| 160 |
+
padding: 15px;
|
| 161 |
+
border-radius: 10px;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.stat-label {
|
| 165 |
+
font-size: 12px;
|
| 166 |
+
opacity: 0.8;
|
| 167 |
+
margin-bottom: 5px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.stat-value {
|
| 171 |
+
font-size: 24px;
|
| 172 |
+
font-weight: 700;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.api-list {
|
| 176 |
+
max-height: 400px;
|
| 177 |
+
overflow-y: auto;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.api-item {
|
| 181 |
+
background: rgba(255, 255, 255, 0.05);
|
| 182 |
+
padding: 15px;
|
| 183 |
+
border-radius: 10px;
|
| 184 |
+
margin-bottom: 10px;
|
| 185 |
+
display: flex;
|
| 186 |
+
justify-content: space-between;
|
| 187 |
+
align-items: center;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.api-info {
|
| 191 |
+
flex: 1;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.api-name {
|
| 195 |
+
font-weight: 600;
|
| 196 |
+
margin-bottom: 5px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.api-meta {
|
| 200 |
+
font-size: 12px;
|
| 201 |
+
opacity: 0.7;
|
| 202 |
+
display: flex;
|
| 203 |
+
gap: 15px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.api-controls {
|
| 207 |
+
display: flex;
|
| 208 |
+
gap: 10px;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.small-btn {
|
| 212 |
+
padding: 5px 12px;
|
| 213 |
+
font-size: 12px;
|
| 214 |
+
border-radius: 5px;
|
| 215 |
+
border: none;
|
| 216 |
+
cursor: pointer;
|
| 217 |
+
background: rgba(255, 255, 255, 0.2);
|
| 218 |
+
color: white;
|
| 219 |
+
transition: all 0.2s;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.small-btn:hover {
|
| 223 |
+
background: rgba(255, 255, 255, 0.3);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.status-badge {
|
| 227 |
+
display: inline-block;
|
| 228 |
+
padding: 4px 10px;
|
| 229 |
+
border-radius: 12px;
|
| 230 |
+
font-size: 11px;
|
| 231 |
+
font-weight: 600;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.status-success {
|
| 235 |
+
background: #10b981;
|
| 236 |
+
color: white;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.status-pending {
|
| 240 |
+
background: #f59e0b;
|
| 241 |
+
color: white;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.status-failed {
|
| 245 |
+
background: #ef4444;
|
| 246 |
+
color: white;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.log-container {
|
| 250 |
+
background: rgba(0, 0, 0, 0.3);
|
| 251 |
+
border-radius: 10px;
|
| 252 |
+
padding: 15px;
|
| 253 |
+
max-height: 300px;
|
| 254 |
+
overflow-y: auto;
|
| 255 |
+
font-family: 'Courier New', monospace;
|
| 256 |
+
font-size: 12px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.log-entry {
|
| 260 |
+
margin-bottom: 8px;
|
| 261 |
+
padding: 5px;
|
| 262 |
+
border-left: 3px solid #667eea;
|
| 263 |
+
padding-left: 10px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.log-time {
|
| 267 |
+
opacity: 0.6;
|
| 268 |
+
margin-right: 10px;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.modal {
|
| 272 |
+
display: none;
|
| 273 |
+
position: fixed;
|
| 274 |
+
top: 0;
|
| 275 |
+
left: 0;
|
| 276 |
+
width: 100%;
|
| 277 |
+
height: 100%;
|
| 278 |
+
background: rgba(0, 0, 0, 0.7);
|
| 279 |
+
z-index: 1000;
|
| 280 |
+
justify-content: center;
|
| 281 |
+
align-items: center;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.modal.active {
|
| 285 |
+
display: flex;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.modal-content {
|
| 289 |
+
background: white;
|
| 290 |
+
border-radius: 15px;
|
| 291 |
+
padding: 30px;
|
| 292 |
+
max-width: 500px;
|
| 293 |
+
width: 90%;
|
| 294 |
+
color: #333;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.modal-header {
|
| 298 |
+
display: flex;
|
| 299 |
+
justify-content: space-between;
|
| 300 |
+
align-items: center;
|
| 301 |
+
margin-bottom: 20px;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.modal-close {
|
| 305 |
+
background: none;
|
| 306 |
+
border: none;
|
| 307 |
+
font-size: 24px;
|
| 308 |
+
cursor: pointer;
|
| 309 |
+
color: #666;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.form-group {
|
| 313 |
+
margin-bottom: 15px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.form-label {
|
| 317 |
+
display: block;
|
| 318 |
+
margin-bottom: 5px;
|
| 319 |
+
font-weight: 600;
|
| 320 |
+
color: #333;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.form-input {
|
| 324 |
+
width: 100%;
|
| 325 |
+
padding: 10px;
|
| 326 |
+
border: 1px solid #ddd;
|
| 327 |
+
border-radius: 8px;
|
| 328 |
+
font-size: 14px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.form-select {
|
| 332 |
+
width: 100%;
|
| 333 |
+
padding: 10px;
|
| 334 |
+
border: 1px solid #ddd;
|
| 335 |
+
border-radius: 8px;
|
| 336 |
+
font-size: 14px;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
::-webkit-scrollbar {
|
| 340 |
+
width: 8px;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
::-webkit-scrollbar-track {
|
| 344 |
+
background: rgba(255, 255, 255, 0.1);
|
| 345 |
+
border-radius: 4px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
::-webkit-scrollbar-thumb {
|
| 349 |
+
background: rgba(255, 255, 255, 0.3);
|
| 350 |
+
border-radius: 4px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
::-webkit-scrollbar-thumb:hover {
|
| 354 |
+
background: rgba(255, 255, 255, 0.5);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.toast {
|
| 358 |
+
position: fixed;
|
| 359 |
+
bottom: 20px;
|
| 360 |
+
right: 20px;
|
| 361 |
+
background: white;
|
| 362 |
+
color: #333;
|
| 363 |
+
padding: 15px 20px;
|
| 364 |
+
border-radius: 10px;
|
| 365 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 366 |
+
opacity: 0;
|
| 367 |
+
transform: translateY(20px);
|
| 368 |
+
transition: all 0.3s ease;
|
| 369 |
+
z-index: 2000;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.toast.show {
|
| 373 |
+
opacity: 1;
|
| 374 |
+
transform: translateY(0);
|
| 375 |
+
}
|
| 376 |
+
</style>
|
| 377 |
+
</head>
|
| 378 |
+
<body>
|
| 379 |
+
<div class="container">
|
| 380 |
+
<header>
|
| 381 |
+
<h1>
|
| 382 |
+
<span>🚀</span>
|
| 383 |
+
Enhanced Crypto Data Tracker
|
| 384 |
+
</h1>
|
| 385 |
+
<div class="connection-status">
|
| 386 |
+
<div class="status-indicator" id="wsStatus"></div>
|
| 387 |
+
<span id="wsStatusText">Connecting...</span>
|
| 388 |
+
</div>
|
| 389 |
+
</header>
|
| 390 |
+
|
| 391 |
+
<div class="controls">
|
| 392 |
+
<div class="controls-row">
|
| 393 |
+
<button class="btn btn-primary" onclick="exportJSON()">
|
| 394 |
+
💾 Export JSON
|
| 395 |
+
</button>
|
| 396 |
+
<button class="btn btn-primary" onclick="exportCSV()">
|
| 397 |
+
📊 Export CSV
|
| 398 |
+
</button>
|
| 399 |
+
<button class="btn btn-success" onclick="createBackup()">
|
| 400 |
+
🔄 Create Backup
|
| 401 |
+
</button>
|
| 402 |
+
<button class="btn btn-info" onclick="showScheduleModal()">
|
| 403 |
+
⏰ Configure Schedule
|
| 404 |
+
</button>
|
| 405 |
+
<button class="btn btn-info" onclick="forceUpdateAll()">
|
| 406 |
+
🔃 Force Update All
|
| 407 |
+
</button>
|
| 408 |
+
<button class="btn btn-danger" onclick="clearCache()">
|
| 409 |
+
🗑️ Clear Cache
|
| 410 |
+
</button>
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
|
| 414 |
+
<div class="grid">
|
| 415 |
+
<div class="card">
|
| 416 |
+
<h2>📊 System Statistics</h2>
|
| 417 |
+
<div class="stat-grid">
|
| 418 |
+
<div class="stat-item">
|
| 419 |
+
<div class="stat-label">Total APIs</div>
|
| 420 |
+
<div class="stat-value" id="totalApis">0</div>
|
| 421 |
+
</div>
|
| 422 |
+
<div class="stat-item">
|
| 423 |
+
<div class="stat-label">Active Tasks</div>
|
| 424 |
+
<div class="stat-value" id="activeTasks">0</div>
|
| 425 |
+
</div>
|
| 426 |
+
<div class="stat-item">
|
| 427 |
+
<div class="stat-label">Cached Data</div>
|
| 428 |
+
<div class="stat-value" id="cachedData">0</div>
|
| 429 |
+
</div>
|
| 430 |
+
<div class="stat-item">
|
| 431 |
+
<div class="stat-label">WS Connections</div>
|
| 432 |
+
<div class="stat-value" id="wsConnections">0</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
</div>
|
| 436 |
+
|
| 437 |
+
<div class="card">
|
| 438 |
+
<h2>📈 Recent Activity</h2>
|
| 439 |
+
<div class="log-container" id="activityLog">
|
| 440 |
+
<div class="log-entry">
|
| 441 |
+
<span class="log-time">--:--:--</span>
|
| 442 |
+
Waiting for updates...
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
|
| 448 |
+
<div class="card">
|
| 449 |
+
<h2>🔌 API Sources</h2>
|
| 450 |
+
<div class="api-list" id="apiList">
|
| 451 |
+
Loading...
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
|
| 456 |
+
<!-- Schedule Modal -->
|
| 457 |
+
<div class="modal" id="scheduleModal">
|
| 458 |
+
<div class="modal-content">
|
| 459 |
+
<div class="modal-header">
|
| 460 |
+
<h2>⏰ Configure Schedule</h2>
|
| 461 |
+
<button class="modal-close" onclick="closeScheduleModal()">×</button>
|
| 462 |
+
</div>
|
| 463 |
+
<div class="form-group">
|
| 464 |
+
<label class="form-label">API Source</label>
|
| 465 |
+
<select class="form-select" id="scheduleApiSelect"></select>
|
| 466 |
+
</div>
|
| 467 |
+
<div class="form-group">
|
| 468 |
+
<label class="form-label">Interval (seconds)</label>
|
| 469 |
+
<input type="number" class="form-input" id="scheduleInterval" value="60" min="10">
|
| 470 |
+
</div>
|
| 471 |
+
<div class="form-group">
|
| 472 |
+
<label class="form-label">Enabled</label>
|
| 473 |
+
<input type="checkbox" id="scheduleEnabled" checked>
|
| 474 |
+
</div>
|
| 475 |
+
<button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
|
| 479 |
+
<!-- Toast notification -->
|
| 480 |
+
<div class="toast" id="toast"></div>
|
| 481 |
+
|
| 482 |
+
<script>
|
| 483 |
+
let ws = null;
|
| 484 |
+
let reconnectAttempts = 0;
|
| 485 |
+
const maxReconnectAttempts = 5;
|
| 486 |
+
const reconnectDelay = 3000;
|
| 487 |
+
|
| 488 |
+
// Initialize WebSocket connection
|
| 489 |
+
function initWebSocket() {
|
| 490 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 491 |
+
const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`;
|
| 492 |
+
|
| 493 |
+
ws = new WebSocket(wsUrl);
|
| 494 |
+
|
| 495 |
+
ws.onopen = () => {
|
| 496 |
+
console.log('WebSocket connected');
|
| 497 |
+
updateWSStatus(true);
|
| 498 |
+
reconnectAttempts = 0;
|
| 499 |
+
|
| 500 |
+
// Subscribe to all updates
|
| 501 |
+
ws.send(JSON.stringify({ type: 'subscribe_all' }));
|
| 502 |
+
|
| 503 |
+
// Start heartbeat
|
| 504 |
+
startHeartbeat();
|
| 505 |
+
};
|
| 506 |
+
|
| 507 |
+
ws.onmessage = (event) => {
|
| 508 |
+
const message = JSON.parse(event.data);
|
| 509 |
+
handleWSMessage(message);
|
| 510 |
+
};
|
| 511 |
+
|
| 512 |
+
ws.onerror = (error) => {
|
| 513 |
+
console.error('WebSocket error:', error);
|
| 514 |
+
};
|
| 515 |
+
|
| 516 |
+
ws.onclose = () => {
|
| 517 |
+
console.log('WebSocket disconnected');
|
| 518 |
+
updateWSStatus(false);
|
| 519 |
+
attemptReconnect();
|
| 520 |
+
};
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function attemptReconnect() {
|
| 524 |
+
if (reconnectAttempts < maxReconnectAttempts) {
|
| 525 |
+
reconnectAttempts++;
|
| 526 |
+
console.log(`Reconnecting... Attempt ${reconnectAttempts}`);
|
| 527 |
+
setTimeout(initWebSocket, reconnectDelay);
|
| 528 |
+
}
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
let heartbeatInterval;
|
| 532 |
+
function startHeartbeat() {
|
| 533 |
+
clearInterval(heartbeatInterval);
|
| 534 |
+
heartbeatInterval = setInterval(() => {
|
| 535 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 536 |
+
ws.send(JSON.stringify({ type: 'ping' }));
|
| 537 |
+
}
|
| 538 |
+
}, 30000);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
function updateWSStatus(connected) {
|
| 542 |
+
const indicator = document.getElementById('wsStatus');
|
| 543 |
+
const text = document.getElementById('wsStatusText');
|
| 544 |
+
|
| 545 |
+
if (connected) {
|
| 546 |
+
indicator.classList.remove('disconnected');
|
| 547 |
+
text.textContent = 'Connected';
|
| 548 |
+
} else {
|
| 549 |
+
indicator.classList.add('disconnected');
|
| 550 |
+
text.textContent = 'Disconnected';
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
function handleWSMessage(message) {
|
| 555 |
+
console.log('Received:', message);
|
| 556 |
+
|
| 557 |
+
switch (message.type) {
|
| 558 |
+
case 'api_update':
|
| 559 |
+
handleApiUpdate(message);
|
| 560 |
+
break;
|
| 561 |
+
case 'status_update':
|
| 562 |
+
handleStatusUpdate(message);
|
| 563 |
+
break;
|
| 564 |
+
case 'schedule_update':
|
| 565 |
+
handleScheduleUpdate(message);
|
| 566 |
+
break;
|
| 567 |
+
case 'subscribed':
|
| 568 |
+
addLog(`Subscribed to ${message.api_id || 'all updates'}`);
|
| 569 |
+
break;
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
function handleApiUpdate(message) {
|
| 574 |
+
addLog(`Updated: ${message.api_id}`, 'success');
|
| 575 |
+
loadSystemStatus();
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
function handleStatusUpdate(message) {
|
| 579 |
+
addLog('System status updated');
|
| 580 |
+
loadSystemStatus();
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
function handleScheduleUpdate(message) {
|
| 584 |
+
addLog(`Schedule updated for ${message.schedule.api_id}`);
|
| 585 |
+
loadAPIs();
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
function addLog(text, type = 'info') {
|
| 589 |
+
const logContainer = document.getElementById('activityLog');
|
| 590 |
+
const time = new Date().toLocaleTimeString();
|
| 591 |
+
|
| 592 |
+
const entry = document.createElement('div');
|
| 593 |
+
entry.className = 'log-entry';
|
| 594 |
+
entry.innerHTML = `<span class="log-time">${time}</span>${text}`;
|
| 595 |
+
|
| 596 |
+
logContainer.insertBefore(entry, logContainer.firstChild);
|
| 597 |
+
|
| 598 |
+
// Keep only last 50 entries
|
| 599 |
+
while (logContainer.children.length > 50) {
|
| 600 |
+
logContainer.removeChild(logContainer.lastChild);
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
function showToast(message, duration = 3000) {
|
| 605 |
+
const toast = document.getElementById('toast');
|
| 606 |
+
toast.textContent = message;
|
| 607 |
+
toast.classList.add('show');
|
| 608 |
+
|
| 609 |
+
setTimeout(() => {
|
| 610 |
+
toast.classList.remove('show');
|
| 611 |
+
}, duration);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
// Load system status
|
| 615 |
+
async function loadSystemStatus() {
|
| 616 |
+
try {
|
| 617 |
+
const response = await fetch('/api/v2/status');
|
| 618 |
+
const data = await response.json();
|
| 619 |
+
|
| 620 |
+
document.getElementById('totalApis').textContent =
|
| 621 |
+
data.services.config_loader.apis_loaded;
|
| 622 |
+
document.getElementById('activeTasks').textContent =
|
| 623 |
+
data.services.scheduler.total_tasks;
|
| 624 |
+
document.getElementById('cachedData').textContent =
|
| 625 |
+
data.services.persistence.cached_apis;
|
| 626 |
+
document.getElementById('wsConnections').textContent =
|
| 627 |
+
data.services.websocket.total_connections;
|
| 628 |
+
|
| 629 |
+
} catch (error) {
|
| 630 |
+
console.error('Error loading status:', error);
|
| 631 |
+
}
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
// Load APIs
|
| 635 |
+
async function loadAPIs() {
|
| 636 |
+
try {
|
| 637 |
+
const response = await fetch('/api/v2/config/apis');
|
| 638 |
+
const data = await response.json();
|
| 639 |
+
|
| 640 |
+
const scheduleResponse = await fetch('/api/v2/schedule/tasks');
|
| 641 |
+
const schedules = await scheduleResponse.json();
|
| 642 |
+
|
| 643 |
+
displayAPIs(data.apis, schedules);
|
| 644 |
+
|
| 645 |
+
} catch (error) {
|
| 646 |
+
console.error('Error loading APIs:', error);
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
function displayAPIs(apis, schedules) {
|
| 651 |
+
const listElement = document.getElementById('apiList');
|
| 652 |
+
listElement.innerHTML = '';
|
| 653 |
+
|
| 654 |
+
for (const [apiId, api] of Object.entries(apis)) {
|
| 655 |
+
const schedule = schedules[apiId] || {};
|
| 656 |
+
|
| 657 |
+
const item = document.createElement('div');
|
| 658 |
+
item.className = 'api-item';
|
| 659 |
+
item.innerHTML = `
|
| 660 |
+
<div class="api-info">
|
| 661 |
+
<div class="api-name">${api.name}</div>
|
| 662 |
+
<div class="api-meta">
|
| 663 |
+
<span>📂 ${api.category}</span>
|
| 664 |
+
<span>⏱️ ${schedule.interval || 300}s</span>
|
| 665 |
+
<span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}">
|
| 666 |
+
${schedule.last_status || 'pending'}
|
| 667 |
+
</span>
|
| 668 |
+
</div>
|
| 669 |
+
</div>
|
| 670 |
+
<div class="api-controls">
|
| 671 |
+
<button class="small-btn" onclick="forceUpdate('${apiId}')">🔄 Update</button>
|
| 672 |
+
<button class="small-btn" onclick="showScheduleModalFor('${apiId}')">⚙️ Schedule</button>
|
| 673 |
+
</div>
|
| 674 |
+
`;
|
| 675 |
+
|
| 676 |
+
listElement.appendChild(item);
|
| 677 |
+
}
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
// Export functions
|
| 681 |
+
async function exportJSON() {
|
| 682 |
+
try {
|
| 683 |
+
const response = await fetch('/api/v2/export/json', {
|
| 684 |
+
method: 'POST',
|
| 685 |
+
headers: { 'Content-Type': 'application/json' },
|
| 686 |
+
body: JSON.stringify({ include_history: true })
|
| 687 |
+
});
|
| 688 |
+
|
| 689 |
+
const data = await response.json();
|
| 690 |
+
showToast('✅ JSON export created!');
|
| 691 |
+
addLog(`Exported to JSON: ${data.filepath}`);
|
| 692 |
+
|
| 693 |
+
// Trigger download
|
| 694 |
+
window.open(data.download_url, '_blank');
|
| 695 |
+
|
| 696 |
+
} catch (error) {
|
| 697 |
+
showToast('❌ Export failed');
|
| 698 |
+
console.error(error);
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
async function exportCSV() {
|
| 703 |
+
try {
|
| 704 |
+
const response = await fetch('/api/v2/export/csv', {
|
| 705 |
+
method: 'POST',
|
| 706 |
+
headers: { 'Content-Type': 'application/json' },
|
| 707 |
+
body: JSON.stringify({ flatten: true })
|
| 708 |
+
});
|
| 709 |
+
|
| 710 |
+
const data = await response.json();
|
| 711 |
+
showToast('✅ CSV export created!');
|
| 712 |
+
addLog(`Exported to CSV: ${data.filepath}`);
|
| 713 |
+
|
| 714 |
+
// Trigger download
|
| 715 |
+
window.open(data.download_url, '_blank');
|
| 716 |
+
|
| 717 |
+
} catch (error) {
|
| 718 |
+
showToast('❌ Export failed');
|
| 719 |
+
console.error(error);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
async function createBackup() {
|
| 724 |
+
try {
|
| 725 |
+
const response = await fetch('/api/v2/backup', {
|
| 726 |
+
method: 'POST'
|
| 727 |
+
});
|
| 728 |
+
|
| 729 |
+
const data = await response.json();
|
| 730 |
+
showToast('✅ Backup created!');
|
| 731 |
+
addLog(`Backup created: ${data.backup_file}`);
|
| 732 |
+
|
| 733 |
+
} catch (error) {
|
| 734 |
+
showToast('❌ Backup failed');
|
| 735 |
+
console.error(error);
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
async function forceUpdate(apiId) {
|
| 740 |
+
try {
|
| 741 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, {
|
| 742 |
+
method: 'POST'
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
const data = await response.json();
|
| 746 |
+
showToast(`✅ ${apiId} updated!`);
|
| 747 |
+
addLog(`Forced update: ${apiId}`);
|
| 748 |
+
loadAPIs();
|
| 749 |
+
|
| 750 |
+
} catch (error) {
|
| 751 |
+
showToast('❌ Update failed');
|
| 752 |
+
console.error(error);
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
async function forceUpdateAll() {
|
| 757 |
+
showToast('🔄 Updating all APIs...');
|
| 758 |
+
addLog('Forcing update for all APIs');
|
| 759 |
+
|
| 760 |
+
try {
|
| 761 |
+
const response = await fetch('/api/v2/config/apis');
|
| 762 |
+
const data = await response.json();
|
| 763 |
+
|
| 764 |
+
for (const apiId of Object.keys(data.apis)) {
|
| 765 |
+
await forceUpdate(apiId);
|
| 766 |
+
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
showToast('✅ All APIs updated!');
|
| 770 |
+
} catch (error) {
|
| 771 |
+
showToast('❌ Update failed');
|
| 772 |
+
console.error(error);
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
async function clearCache() {
|
| 777 |
+
if (!confirm('Clear all cached data?')) return;
|
| 778 |
+
|
| 779 |
+
try {
|
| 780 |
+
const response = await fetch('/api/v2/cleanup/cache', {
|
| 781 |
+
method: 'POST'
|
| 782 |
+
});
|
| 783 |
+
|
| 784 |
+
showToast('✅ Cache cleared!');
|
| 785 |
+
addLog('Cache cleared');
|
| 786 |
+
loadSystemStatus();
|
| 787 |
+
|
| 788 |
+
} catch (error) {
|
| 789 |
+
showToast('❌ Failed to clear cache');
|
| 790 |
+
console.error(error);
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
// Schedule modal functions
|
| 795 |
+
function showScheduleModal() {
|
| 796 |
+
loadAPISelectOptions();
|
| 797 |
+
document.getElementById('scheduleModal').classList.add('active');
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
function closeScheduleModal() {
|
| 801 |
+
document.getElementById('scheduleModal').classList.remove('active');
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
async function showScheduleModalFor(apiId) {
|
| 805 |
+
await loadAPISelectOptions();
|
| 806 |
+
document.getElementById('scheduleApiSelect').value = apiId;
|
| 807 |
+
|
| 808 |
+
// Load current schedule
|
| 809 |
+
try {
|
| 810 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}`);
|
| 811 |
+
const schedule = await response.json();
|
| 812 |
+
|
| 813 |
+
document.getElementById('scheduleInterval').value = schedule.interval;
|
| 814 |
+
document.getElementById('scheduleEnabled').checked = schedule.enabled;
|
| 815 |
+
|
| 816 |
+
} catch (error) {
|
| 817 |
+
console.error(error);
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
showScheduleModal();
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
async function loadAPISelectOptions() {
|
| 824 |
+
try {
|
| 825 |
+
const response = await fetch('/api/v2/config/apis');
|
| 826 |
+
const data = await response.json();
|
| 827 |
+
|
| 828 |
+
const select = document.getElementById('scheduleApiSelect');
|
| 829 |
+
select.innerHTML = '';
|
| 830 |
+
|
| 831 |
+
for (const [apiId, api] of Object.entries(data.apis)) {
|
| 832 |
+
const option = document.createElement('option');
|
| 833 |
+
option.value = apiId;
|
| 834 |
+
option.textContent = api.name;
|
| 835 |
+
select.appendChild(option);
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
} catch (error) {
|
| 839 |
+
console.error(error);
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
async function updateSchedule() {
|
| 844 |
+
const apiId = document.getElementById('scheduleApiSelect').value;
|
| 845 |
+
const interval = parseInt(document.getElementById('scheduleInterval').value);
|
| 846 |
+
const enabled = document.getElementById('scheduleEnabled').checked;
|
| 847 |
+
|
| 848 |
+
try {
|
| 849 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, {
|
| 850 |
+
method: 'PUT'
|
| 851 |
+
});
|
| 852 |
+
|
| 853 |
+
const data = await response.json();
|
| 854 |
+
showToast('✅ Schedule updated!');
|
| 855 |
+
addLog(`Updated schedule for ${apiId}`);
|
| 856 |
+
closeScheduleModal();
|
| 857 |
+
loadAPIs();
|
| 858 |
+
|
| 859 |
+
} catch (error) {
|
| 860 |
+
showToast('❌ Schedule update failed');
|
| 861 |
+
console.error(error);
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
// Initialize on load
|
| 866 |
+
window.addEventListener('load', () => {
|
| 867 |
+
initWebSocket();
|
| 868 |
+
loadSystemStatus();
|
| 869 |
+
loadAPIs();
|
| 870 |
+
|
| 871 |
+
// Refresh status every 30 seconds
|
| 872 |
+
setInterval(loadSystemStatus, 30000);
|
| 873 |
+
});
|
| 874 |
+
</script>
|
| 875 |
+
</body>
|
| 876 |
+
</html>
|
archive_html/feature_flags_demo.html
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto Monitor - Feature Flags Demo</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/mobile-responsive.css">
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 17 |
+
background: #f5f7fa;
|
| 18 |
+
padding: 20px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.header {
|
| 22 |
+
background: #fff;
|
| 23 |
+
padding: 20px;
|
| 24 |
+
border-radius: 8px;
|
| 25 |
+
margin-bottom: 20px;
|
| 26 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
h1 {
|
| 30 |
+
color: #333;
|
| 31 |
+
margin-bottom: 10px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.subtitle {
|
| 35 |
+
color: #666;
|
| 36 |
+
font-size: 0.95rem;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.dashboard {
|
| 40 |
+
display: grid;
|
| 41 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 42 |
+
gap: 20px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.card {
|
| 46 |
+
background: #fff;
|
| 47 |
+
border-radius: 8px;
|
| 48 |
+
padding: 20px;
|
| 49 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.card h3 {
|
| 53 |
+
margin-bottom: 15px;
|
| 54 |
+
color: #333;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.stats-grid {
|
| 58 |
+
display: grid;
|
| 59 |
+
grid-template-columns: repeat(2, 1fr);
|
| 60 |
+
gap: 15px;
|
| 61 |
+
margin-top: 15px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.stat-item {
|
| 65 |
+
padding: 15px;
|
| 66 |
+
background: #f8f9fa;
|
| 67 |
+
border-radius: 6px;
|
| 68 |
+
text-align: center;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.stat-label {
|
| 72 |
+
font-size: 0.85rem;
|
| 73 |
+
color: #666;
|
| 74 |
+
margin-bottom: 8px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.stat-value {
|
| 78 |
+
font-size: 1.8rem;
|
| 79 |
+
font-weight: 700;
|
| 80 |
+
color: #333;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.btn {
|
| 84 |
+
padding: 10px 20px;
|
| 85 |
+
background: #007bff;
|
| 86 |
+
color: #fff;
|
| 87 |
+
border: none;
|
| 88 |
+
border-radius: 4px;
|
| 89 |
+
cursor: pointer;
|
| 90 |
+
font-size: 0.95rem;
|
| 91 |
+
transition: background 0.2s;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.btn:hover {
|
| 95 |
+
background: #0056b3;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.btn-secondary {
|
| 99 |
+
background: #6c757d;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.btn-secondary:hover {
|
| 103 |
+
background: #5a6268;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.provider-list {
|
| 107 |
+
display: flex;
|
| 108 |
+
flex-direction: column;
|
| 109 |
+
gap: 10px;
|
| 110 |
+
margin-top: 15px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.provider-item {
|
| 114 |
+
padding: 12px;
|
| 115 |
+
background: #f8f9fa;
|
| 116 |
+
border-radius: 6px;
|
| 117 |
+
display: flex;
|
| 118 |
+
justify-content: space-between;
|
| 119 |
+
align-items: center;
|
| 120 |
+
border-left: 4px solid #ccc;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.provider-item.online {
|
| 124 |
+
border-left-color: #28a745;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.provider-item.degraded {
|
| 128 |
+
border-left-color: #ffc107;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.provider-item.offline {
|
| 132 |
+
border-left-color: #dc3545;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.provider-info {
|
| 136 |
+
flex: 1;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.provider-name {
|
| 140 |
+
font-weight: 600;
|
| 141 |
+
color: #333;
|
| 142 |
+
margin-bottom: 4px;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.provider-meta {
|
| 146 |
+
font-size: 0.85rem;
|
| 147 |
+
color: #666;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.proxy-indicator {
|
| 151 |
+
display: inline-flex;
|
| 152 |
+
align-items: center;
|
| 153 |
+
gap: 4px;
|
| 154 |
+
padding: 4px 8px;
|
| 155 |
+
background: #fff3cd;
|
| 156 |
+
color: #856404;
|
| 157 |
+
border-radius: 4px;
|
| 158 |
+
font-size: 0.75rem;
|
| 159 |
+
margin-left: 8px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.footer {
|
| 163 |
+
margin-top: 30px;
|
| 164 |
+
text-align: center;
|
| 165 |
+
color: #666;
|
| 166 |
+
font-size: 0.85rem;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
@media screen and (max-width: 768px) {
|
| 170 |
+
.dashboard {
|
| 171 |
+
grid-template-columns: 1fr;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.stats-grid {
|
| 175 |
+
grid-template-columns: 1fr;
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
</style>
|
| 179 |
+
</head>
|
| 180 |
+
<body>
|
| 181 |
+
<div class="header">
|
| 182 |
+
<h1>🚀 Crypto Monitor - Feature Flags Demo</h1>
|
| 183 |
+
<p class="subtitle">Enterprise-Grade API Monitoring with Smart Proxy Mode</p>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div class="dashboard">
|
| 187 |
+
<!-- Feature Flags Card -->
|
| 188 |
+
<div class="card">
|
| 189 |
+
<div id="feature-flags-container"></div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<!-- System Status Card -->
|
| 193 |
+
<div class="card">
|
| 194 |
+
<h3>📊 System Status</h3>
|
| 195 |
+
<div class="stats-grid">
|
| 196 |
+
<div class="stat-item">
|
| 197 |
+
<div class="stat-label">Total Providers</div>
|
| 198 |
+
<div class="stat-value" id="stat-total">-</div>
|
| 199 |
+
</div>
|
| 200 |
+
<div class="stat-item">
|
| 201 |
+
<div class="stat-label">Online</div>
|
| 202 |
+
<div class="stat-value" id="stat-online" style="color: #28a745;">-</div>
|
| 203 |
+
</div>
|
| 204 |
+
<div class="stat-item">
|
| 205 |
+
<div class="stat-label">Using Proxy</div>
|
| 206 |
+
<div class="stat-value" id="stat-proxy" style="color: #ffc107;">-</div>
|
| 207 |
+
</div>
|
| 208 |
+
<div class="stat-item">
|
| 209 |
+
<div class="stat-label">Avg Response</div>
|
| 210 |
+
<div class="stat-value" id="stat-response">-</div>
|
| 211 |
+
</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<!-- Provider Health Card -->
|
| 216 |
+
<div class="card" style="grid-column: span 2;">
|
| 217 |
+
<h3>🔧 Provider Health Status</h3>
|
| 218 |
+
<div class="provider-list" id="provider-list">
|
| 219 |
+
<p style="color: #666;">Loading providers...</p>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<!-- Proxy Status Card -->
|
| 224 |
+
<div class="card">
|
| 225 |
+
<h3>🌐 Smart Proxy Status</h3>
|
| 226 |
+
<div id="proxy-status">
|
| 227 |
+
<p style="color: #666;">Loading proxy status...</p>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<div class="footer">
|
| 233 |
+
<p>Crypto Monitor ULTIMATE - Enterprise Edition © 2025</p>
|
| 234 |
+
<p>Powered by Real APIs • Smart Proxy Mode • Feature Flags</p>
|
| 235 |
+
</div>
|
| 236 |
+
|
| 237 |
+
<!-- Mobile Navigation (shows on mobile only) -->
|
| 238 |
+
<div class="mobile-nav-bottom">
|
| 239 |
+
<div class="nav-items">
|
| 240 |
+
<div class="nav-item">
|
| 241 |
+
<a href="#" class="nav-link active">
|
| 242 |
+
<span class="nav-icon">📊</span>
|
| 243 |
+
<span>Dashboard</span>
|
| 244 |
+
</a>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="nav-item">
|
| 247 |
+
<a href="#" class="nav-link">
|
| 248 |
+
<span class="nav-icon">🔧</span>
|
| 249 |
+
<span>Providers</span>
|
| 250 |
+
</a>
|
| 251 |
+
</div>
|
| 252 |
+
<div class="nav-item">
|
| 253 |
+
<a href="#" class="nav-link">
|
| 254 |
+
<span class="nav-icon">⚙️</span>
|
| 255 |
+
<span>Settings</span>
|
| 256 |
+
</a>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<script src="/static/js/feature-flags.js"></script>
|
| 262 |
+
<script>
|
| 263 |
+
// Initialize Feature Flags UI
|
| 264 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 265 |
+
await window.featureFlagsManager.init();
|
| 266 |
+
window.featureFlagsManager.renderUI('feature-flags-container');
|
| 267 |
+
|
| 268 |
+
// Load system status
|
| 269 |
+
loadSystemStatus();
|
| 270 |
+
|
| 271 |
+
// Load provider health
|
| 272 |
+
loadProviderHealth();
|
| 273 |
+
|
| 274 |
+
// Load proxy status
|
| 275 |
+
loadProxyStatus();
|
| 276 |
+
|
| 277 |
+
// Auto-refresh every 30 seconds
|
| 278 |
+
setInterval(() => {
|
| 279 |
+
loadSystemStatus();
|
| 280 |
+
loadProviderHealth();
|
| 281 |
+
loadProxyStatus();
|
| 282 |
+
}, 30000);
|
| 283 |
+
});
|
| 284 |
+
|
| 285 |
+
async function loadSystemStatus() {
|
| 286 |
+
try {
|
| 287 |
+
const response = await fetch('/api/status');
|
| 288 |
+
const data = await response.json();
|
| 289 |
+
|
| 290 |
+
document.getElementById('stat-total').textContent = data.total_providers || 0;
|
| 291 |
+
document.getElementById('stat-online').textContent = data.online || 0;
|
| 292 |
+
document.getElementById('stat-response').textContent =
|
| 293 |
+
data.avg_response_time_ms ? `${Math.round(data.avg_response_time_ms)}ms` : '-';
|
| 294 |
+
|
| 295 |
+
// Get proxy status
|
| 296 |
+
const proxyResp = await fetch('/api/proxy-status');
|
| 297 |
+
const proxyData = await proxyResp.json();
|
| 298 |
+
document.getElementById('stat-proxy').textContent =
|
| 299 |
+
proxyData.total_providers_using_proxy || 0;
|
| 300 |
+
} catch (error) {
|
| 301 |
+
console.error('Error loading system status:', error);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
async function loadProviderHealth() {
|
| 306 |
+
try {
|
| 307 |
+
const response = await fetch('/api/providers');
|
| 308 |
+
const providers = await response.json();
|
| 309 |
+
|
| 310 |
+
if (!Array.isArray(providers)) {
|
| 311 |
+
return;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
const listEl = document.getElementById('provider-list');
|
| 315 |
+
let html = '';
|
| 316 |
+
|
| 317 |
+
providers.slice(0, 10).forEach(provider => {
|
| 318 |
+
const status = provider.status || 'unknown';
|
| 319 |
+
const statusClass = status.toLowerCase();
|
| 320 |
+
|
| 321 |
+
html += `
|
| 322 |
+
<div class="provider-item ${statusClass}">
|
| 323 |
+
<div class="provider-info">
|
| 324 |
+
<div class="provider-name">${provider.name}</div>
|
| 325 |
+
<div class="provider-meta">
|
| 326 |
+
${provider.category || 'unknown'} •
|
| 327 |
+
${provider.response_time_ms ? provider.response_time_ms + 'ms' : 'N/A'}
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
<div>
|
| 331 |
+
<span class="provider-status-badge ${statusClass}">
|
| 332 |
+
${status === 'online' ? '✓' : status === 'degraded' ? '⚠' : '✗'}
|
| 333 |
+
${status.toUpperCase()}
|
| 334 |
+
</span>
|
| 335 |
+
</div>
|
| 336 |
+
</div>
|
| 337 |
+
`;
|
| 338 |
+
});
|
| 339 |
+
|
| 340 |
+
listEl.innerHTML = html || '<p style="color: #666;">No providers found</p>';
|
| 341 |
+
} catch (error) {
|
| 342 |
+
console.error('Error loading provider health:', error);
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
async function loadProxyStatus() {
|
| 347 |
+
try {
|
| 348 |
+
const response = await fetch('/api/proxy-status');
|
| 349 |
+
const data = await response.json();
|
| 350 |
+
|
| 351 |
+
const statusEl = document.getElementById('proxy-status');
|
| 352 |
+
|
| 353 |
+
let html = `
|
| 354 |
+
<div style="margin-bottom: 15px;">
|
| 355 |
+
<strong>Auto Mode:</strong>
|
| 356 |
+
<span style="color: ${data.proxy_auto_mode_enabled ? '#28a745' : '#dc3545'}">
|
| 357 |
+
${data.proxy_auto_mode_enabled ? '✓ Enabled' : '✗ Disabled'}
|
| 358 |
+
</span>
|
| 359 |
+
</div>
|
| 360 |
+
<div style="margin-bottom: 15px;">
|
| 361 |
+
<strong>Providers Using Proxy:</strong> ${data.total_providers_using_proxy}
|
| 362 |
+
</div>
|
| 363 |
+
`;
|
| 364 |
+
|
| 365 |
+
if (data.providers && data.providers.length > 0) {
|
| 366 |
+
html += '<div style="margin-top: 15px;"><strong>Currently Proxied:</strong></div>';
|
| 367 |
+
html += '<div class="provider-list">';
|
| 368 |
+
data.providers.forEach(p => {
|
| 369 |
+
html += `
|
| 370 |
+
<div class="provider-item">
|
| 371 |
+
<div class="provider-info">
|
| 372 |
+
<div class="provider-name">${p.provider}</div>
|
| 373 |
+
<div class="provider-meta">
|
| 374 |
+
${p.reason} • Cached ${p.cache_age_seconds}s ago
|
| 375 |
+
</div>
|
| 376 |
+
</div>
|
| 377 |
+
<div class="proxy-indicator">🌐 PROXY</div>
|
| 378 |
+
</div>
|
| 379 |
+
`;
|
| 380 |
+
});
|
| 381 |
+
html += '</div>';
|
| 382 |
+
} else {
|
| 383 |
+
html += '<p style="color: #666; margin-top: 15px;">No providers currently using proxy</p>';
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
statusEl.innerHTML = html;
|
| 387 |
+
} catch (error) {
|
| 388 |
+
console.error('Error loading proxy status:', error);
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
</script>
|
| 392 |
+
</body>
|
| 393 |
+
</html>
|
archive_html/hf_console.html
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>HF Console · Crypto Intelligence</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
| 10 |
+
<link rel="stylesheet" href="/static/css/unified-ui.css" />
|
| 11 |
+
<link rel="stylesheet" href="/static/css/components.css" />
|
| 12 |
+
<script defer src="/static/js/ui-feedback.js"></script>
|
| 13 |
+
<script defer src="/static/js/hf-console.js"></script>
|
| 14 |
+
</head>
|
| 15 |
+
<body class="page page-hf-console">
|
| 16 |
+
<header class="top-nav">
|
| 17 |
+
<div class="branding">
|
| 18 |
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/></svg>
|
| 19 |
+
<div>
|
| 20 |
+
<strong>HF Models & Datasets</strong>
|
| 21 |
+
<small style="color:var(--ui-text-muted);letter-spacing:0.2em;">/api/hf/* endpoints</small>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<nav class="nav-links">
|
| 25 |
+
<a href="/dashboard">Dashboard</a>
|
| 26 |
+
<a href="/admin">Admin</a>
|
| 27 |
+
<a class="active" href="/hf_console">HF Console</a>
|
| 28 |
+
<a href="/docs" target="_blank" rel="noreferrer">API Docs</a>
|
| 29 |
+
</nav>
|
| 30 |
+
</header>
|
| 31 |
+
|
| 32 |
+
<main class="page-content">
|
| 33 |
+
<section class="card">
|
| 34 |
+
<div class="section-heading">
|
| 35 |
+
<h2>Registry & Status</h2>
|
| 36 |
+
<span class="badge info" id="hf-console-health">Loading...</span>
|
| 37 |
+
</div>
|
| 38 |
+
<p class="metric-subtext" id="hf-console-summary"></p>
|
| 39 |
+
<ul class="list" id="hf-console-models"></ul>
|
| 40 |
+
</section>
|
| 41 |
+
|
| 42 |
+
<section class="split-grid">
|
| 43 |
+
<article class="card">
|
| 44 |
+
<div class="section-heading"><h2>Sentiment Playground</h2><span class="badge info">POST /api/hf/models/sentiment</span></div>
|
| 45 |
+
<div class="form-field">
|
| 46 |
+
<label for="sentiment-model">Sentiment model</label>
|
| 47 |
+
<select id="sentiment-model">
|
| 48 |
+
<option value="auto">auto (ensemble)</option>
|
| 49 |
+
<option value="cryptobert">cryptobert</option>
|
| 50 |
+
<option value="cryptobert_finbert">cryptobert_finbert</option>
|
| 51 |
+
<option value="tiny_crypto_lm">tiny_crypto_lm</option>
|
| 52 |
+
</select>
|
| 53 |
+
</div>
|
| 54 |
+
<div class="form-field">
|
| 55 |
+
<label for="sentiment-texts">Texts (one per line)</label>
|
| 56 |
+
<textarea id="sentiment-texts" rows="5" placeholder="BTC is breaking out...\nETH looks weak..."></textarea>
|
| 57 |
+
</div>
|
| 58 |
+
<button class="primary" id="run-sentiment">Run Sentiment</button>
|
| 59 |
+
<div id="sentiment-results" class="ws-stream" style="margin-top:16px;"></div>
|
| 60 |
+
</article>
|
| 61 |
+
<article class="card">
|
| 62 |
+
<div class="section-heading"><h2>Forecast Sandbox</h2><span class="badge info">POST /api/hf/models/forecast</span></div>
|
| 63 |
+
<div class="form-field">
|
| 64 |
+
<label for="forecast-model">Model</label>
|
| 65 |
+
<select id="forecast-model">
|
| 66 |
+
<option value="btc_lstm">btc_lstm</option>
|
| 67 |
+
<option value="btc_arima">btc_arima</option>
|
| 68 |
+
</select>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="form-field">
|
| 71 |
+
<label for="forecast-series">Closing Prices (comma separated)</label>
|
| 72 |
+
<textarea id="forecast-series" rows="5" placeholder="67650, 67820, 68010, 68120"></textarea>
|
| 73 |
+
</div>
|
| 74 |
+
<div class="form-field">
|
| 75 |
+
<label for="forecast-steps">Future Steps</label>
|
| 76 |
+
<input type="number" id="forecast-steps" value="3" min="1" max="10" />
|
| 77 |
+
</div>
|
| 78 |
+
<button class="primary" id="run-forecast">Forecast</button>
|
| 79 |
+
<div id="forecast-results" class="ws-stream" style="margin-top:16px;"></div>
|
| 80 |
+
</article>
|
| 81 |
+
</section>
|
| 82 |
+
|
| 83 |
+
<section class="card">
|
| 84 |
+
<div class="section-heading">
|
| 85 |
+
<h2>HF Datasets</h2>
|
| 86 |
+
<span class="badge info">GET /api/hf/datasets/*</span>
|
| 87 |
+
</div>
|
| 88 |
+
<div class="button-row" style="margin-bottom:16px;">
|
| 89 |
+
<button class="secondary" data-dataset="market-ohlcv">Market OHLCV</button>
|
| 90 |
+
<button class="secondary" data-dataset="market-btc">BTC Technicals</button>
|
| 91 |
+
<button class="secondary" data-dataset="news-semantic">News Semantic</button>
|
| 92 |
+
</div>
|
| 93 |
+
<div id="dataset-output" class="ws-stream"></div>
|
| 94 |
+
</section>
|
| 95 |
+
</main>
|
| 96 |
+
</body>
|
| 97 |
+
</html>
|
archive_html/improved_dashboard.html
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto Monitor - Complete Overview</title>
|
| 7 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 17 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.container {
|
| 23 |
+
max-width: 1600px;
|
| 24 |
+
margin: 0 auto;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.header {
|
| 28 |
+
background: white;
|
| 29 |
+
border-radius: 15px;
|
| 30 |
+
padding: 30px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.header h1 {
|
| 36 |
+
color: #667eea;
|
| 37 |
+
font-size: 2.5em;
|
| 38 |
+
margin-bottom: 10px;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.header p {
|
| 42 |
+
color: #666;
|
| 43 |
+
font-size: 1.1em;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.stats-grid {
|
| 47 |
+
display: grid;
|
| 48 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 49 |
+
gap: 20px;
|
| 50 |
+
margin-bottom: 20px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.stat-card {
|
| 54 |
+
background: white;
|
| 55 |
+
border-radius: 15px;
|
| 56 |
+
padding: 25px;
|
| 57 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 58 |
+
transition: transform 0.3s ease;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.stat-card:hover {
|
| 62 |
+
transform: translateY(-5px);
|
| 63 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.stat-card h3 {
|
| 67 |
+
color: #999;
|
| 68 |
+
font-size: 0.9em;
|
| 69 |
+
text-transform: uppercase;
|
| 70 |
+
margin-bottom: 10px;
|
| 71 |
+
font-weight: 600;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.stat-card .value {
|
| 75 |
+
font-size: 2.5em;
|
| 76 |
+
font-weight: bold;
|
| 77 |
+
margin-bottom: 5px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.stat-card .label {
|
| 81 |
+
color: #666;
|
| 82 |
+
font-size: 0.9em;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.stat-card.green .value { color: #10b981; }
|
| 86 |
+
.stat-card.blue .value { color: #3b82f6; }
|
| 87 |
+
.stat-card.purple .value { color: #8b5cf6; }
|
| 88 |
+
.stat-card.orange .value { color: #f59e0b; }
|
| 89 |
+
.stat-card.red .value { color: #ef4444; }
|
| 90 |
+
|
| 91 |
+
.main-grid {
|
| 92 |
+
display: grid;
|
| 93 |
+
grid-template-columns: 2fr 1fr;
|
| 94 |
+
gap: 20px;
|
| 95 |
+
margin-bottom: 20px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.card {
|
| 99 |
+
background: white;
|
| 100 |
+
border-radius: 15px;
|
| 101 |
+
padding: 25px;
|
| 102 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.card h2 {
|
| 106 |
+
color: #333;
|
| 107 |
+
margin-bottom: 20px;
|
| 108 |
+
font-size: 1.5em;
|
| 109 |
+
border-bottom: 3px solid #667eea;
|
| 110 |
+
padding-bottom: 10px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.providers-grid {
|
| 114 |
+
display: grid;
|
| 115 |
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
| 116 |
+
gap: 15px;
|
| 117 |
+
max-height: 500px;
|
| 118 |
+
overflow-y: auto;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.provider-card {
|
| 122 |
+
background: #f8f9fa;
|
| 123 |
+
border-radius: 10px;
|
| 124 |
+
padding: 15px;
|
| 125 |
+
border-left: 4px solid #ddd;
|
| 126 |
+
transition: all 0.3s ease;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.provider-card:hover {
|
| 130 |
+
transform: translateX(5px);
|
| 131 |
+
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.provider-card.online { border-left-color: #10b981; background: #f0fdf4; }
|
| 135 |
+
.provider-card.offline { border-left-color: #ef4444; background: #fef2f2; }
|
| 136 |
+
.provider-card.degraded { border-left-color: #f59e0b; background: #fffbeb; }
|
| 137 |
+
|
| 138 |
+
.provider-card .name {
|
| 139 |
+
font-weight: 600;
|
| 140 |
+
color: #333;
|
| 141 |
+
margin-bottom: 5px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.provider-card .category {
|
| 145 |
+
font-size: 0.85em;
|
| 146 |
+
color: #666;
|
| 147 |
+
margin-bottom: 5px;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.provider-card .status {
|
| 151 |
+
font-size: 0.8em;
|
| 152 |
+
padding: 3px 8px;
|
| 153 |
+
border-radius: 5px;
|
| 154 |
+
display: inline-block;
|
| 155 |
+
font-weight: 600;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.status.online { background: #10b981; color: white; }
|
| 159 |
+
.status.offline { background: #ef4444; color: white; }
|
| 160 |
+
.status.degraded { background: #f59e0b; color: white; }
|
| 161 |
+
|
| 162 |
+
.category-list {
|
| 163 |
+
display: flex;
|
| 164 |
+
flex-direction: column;
|
| 165 |
+
gap: 15px;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.category-item {
|
| 169 |
+
background: #f8f9fa;
|
| 170 |
+
border-radius: 10px;
|
| 171 |
+
padding: 15px;
|
| 172 |
+
border-left: 4px solid #667eea;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.category-item .cat-name {
|
| 176 |
+
font-weight: 600;
|
| 177 |
+
color: #333;
|
| 178 |
+
margin-bottom: 10px;
|
| 179 |
+
font-size: 1.1em;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.category-item .cat-stats {
|
| 183 |
+
display: flex;
|
| 184 |
+
gap: 15px;
|
| 185 |
+
font-size: 0.9em;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.category-item .cat-stat {
|
| 189 |
+
display: flex;
|
| 190 |
+
align-items: center;
|
| 191 |
+
gap: 5px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.chart-container {
|
| 195 |
+
position: relative;
|
| 196 |
+
height: 300px;
|
| 197 |
+
margin-top: 20px;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.loading {
|
| 201 |
+
text-align: center;
|
| 202 |
+
padding: 40px;
|
| 203 |
+
color: #666;
|
| 204 |
+
font-size: 1.2em;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.refresh-btn {
|
| 208 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 209 |
+
color: white;
|
| 210 |
+
border: none;
|
| 211 |
+
padding: 12px 30px;
|
| 212 |
+
border-radius: 10px;
|
| 213 |
+
font-size: 1em;
|
| 214 |
+
font-weight: 600;
|
| 215 |
+
cursor: pointer;
|
| 216 |
+
transition: all 0.3s ease;
|
| 217 |
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.refresh-btn:hover {
|
| 221 |
+
transform: translateY(-2px);
|
| 222 |
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.refresh-btn:active {
|
| 226 |
+
transform: translateY(0);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
@keyframes pulse {
|
| 230 |
+
0%, 100% { opacity: 1; }
|
| 231 |
+
50% { opacity: 0.5; }
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.updating {
|
| 235 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.full-width {
|
| 239 |
+
grid-column: 1 / -1;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
@media (max-width: 768px) {
|
| 243 |
+
.main-grid {
|
| 244 |
+
grid-template-columns: 1fr;
|
| 245 |
+
}
|
| 246 |
+
.stats-grid {
|
| 247 |
+
grid-template-columns: 1fr;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
</style>
|
| 251 |
+
</head>
|
| 252 |
+
<body>
|
| 253 |
+
<div class="container">
|
| 254 |
+
<div class="header">
|
| 255 |
+
<h1>🚀 Crypto API Monitor</h1>
|
| 256 |
+
<p>Complete Real-Time Overview of All Cryptocurrency Data Sources</p>
|
| 257 |
+
<button class="refresh-btn" onclick="refreshAll()">🔄 Refresh Data</button>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<div class="stats-grid">
|
| 261 |
+
<div class="stat-card green">
|
| 262 |
+
<h3>Total Providers</h3>
|
| 263 |
+
<div class="value" id="totalProviders">-</div>
|
| 264 |
+
<div class="label">API Sources</div>
|
| 265 |
+
</div>
|
| 266 |
+
<div class="stat-card blue">
|
| 267 |
+
<h3>Online</h3>
|
| 268 |
+
<div class="value" id="onlineProviders">-</div>
|
| 269 |
+
<div class="label">Active & Working</div>
|
| 270 |
+
</div>
|
| 271 |
+
<div class="stat-card orange">
|
| 272 |
+
<h3>Degraded</h3>
|
| 273 |
+
<div class="value" id="degradedProviders">-</div>
|
| 274 |
+
<div class="label">Slow Response</div>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="stat-card red">
|
| 277 |
+
<h3>Offline</h3>
|
| 278 |
+
<div class="value" id="offlineProviders">-</div>
|
| 279 |
+
<div class="label">Not Responding</div>
|
| 280 |
+
</div>
|
| 281 |
+
<div class="stat-card purple">
|
| 282 |
+
<h3>Categories</h3>
|
| 283 |
+
<div class="value" id="totalCategories">-</div>
|
| 284 |
+
<div class="label">Data Types</div>
|
| 285 |
+
</div>
|
| 286 |
+
<div class="stat-card green">
|
| 287 |
+
<h3>Uptime</h3>
|
| 288 |
+
<div class="value" id="uptimePercent">-</div>
|
| 289 |
+
<div class="label">Overall Health</div>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
|
| 293 |
+
<div class="main-grid">
|
| 294 |
+
<div class="card">
|
| 295 |
+
<h2>📊 All Providers Status</h2>
|
| 296 |
+
<div class="providers-grid" id="providersGrid">
|
| 297 |
+
<div class="loading">Loading providers...</div>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<div class="card">
|
| 302 |
+
<h2>📁 Categories</h2>
|
| 303 |
+
<div class="category-list" id="categoryList">
|
| 304 |
+
<div class="loading">Loading categories...</div>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</div>
|
| 308 |
+
|
| 309 |
+
<div class="card full-width">
|
| 310 |
+
<h2>📈 Status Distribution</h2>
|
| 311 |
+
<div class="chart-container">
|
| 312 |
+
<canvas id="statusChart"></canvas>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<script>
|
| 318 |
+
let statusChart = null;
|
| 319 |
+
|
| 320 |
+
async function loadProviders() {
|
| 321 |
+
try {
|
| 322 |
+
const response = await fetch('/api/providers');
|
| 323 |
+
const providers = await response.json();
|
| 324 |
+
|
| 325 |
+
// Update stats
|
| 326 |
+
const online = providers.filter(p => p.status === 'online').length;
|
| 327 |
+
const offline = providers.filter(p => p.status === 'offline').length;
|
| 328 |
+
const degraded = providers.filter(p => p.status === 'degraded').length;
|
| 329 |
+
const uptime = ((online / providers.length) * 100).toFixed(1);
|
| 330 |
+
|
| 331 |
+
document.getElementById('totalProviders').textContent = providers.length;
|
| 332 |
+
document.getElementById('onlineProviders').textContent = online;
|
| 333 |
+
document.getElementById('degradedProviders').textContent = degraded;
|
| 334 |
+
document.getElementById('offlineProviders').textContent = offline;
|
| 335 |
+
document.getElementById('uptimePercent').textContent = uptime + '%';
|
| 336 |
+
|
| 337 |
+
// Group by category
|
| 338 |
+
const categories = {};
|
| 339 |
+
providers.forEach(p => {
|
| 340 |
+
if (!categories[p.category]) {
|
| 341 |
+
categories[p.category] = { total: 0, online: 0, offline: 0, degraded: 0 };
|
| 342 |
+
}
|
| 343 |
+
categories[p.category].total++;
|
| 344 |
+
categories[p.category][p.status]++;
|
| 345 |
+
});
|
| 346 |
+
|
| 347 |
+
document.getElementById('totalCategories').textContent = Object.keys(categories).length;
|
| 348 |
+
|
| 349 |
+
// Display providers
|
| 350 |
+
const providersGrid = document.getElementById('providersGrid');
|
| 351 |
+
providersGrid.innerHTML = providers.map(p => `
|
| 352 |
+
<div class="provider-card ${p.status}">
|
| 353 |
+
<div class="name">${p.name}</div>
|
| 354 |
+
<div class="category">${p.category}</div>
|
| 355 |
+
<span class="status ${p.status}">${p.status.toUpperCase()}</span>
|
| 356 |
+
${p.response_time_ms ? `<div style="font-size: 0.8em; color: #666; margin-top: 5px;">${Math.round(p.response_time_ms)}ms</div>` : ''}
|
| 357 |
+
</div>
|
| 358 |
+
`).join('');
|
| 359 |
+
|
| 360 |
+
// Display categories
|
| 361 |
+
const categoryList = document.getElementById('categoryList');
|
| 362 |
+
categoryList.innerHTML = Object.entries(categories).map(([name, stats]) => `
|
| 363 |
+
<div class="category-item">
|
| 364 |
+
<div class="cat-name">${name}</div>
|
| 365 |
+
<div class="cat-stats">
|
| 366 |
+
<div class="cat-stat">
|
| 367 |
+
<span style="color: #10b981;">●</span>
|
| 368 |
+
<span>${stats.online} online</span>
|
| 369 |
+
</div>
|
| 370 |
+
<div class="cat-stat">
|
| 371 |
+
<span style="color: #f59e0b;">●</span>
|
| 372 |
+
<span>${stats.degraded} degraded</span>
|
| 373 |
+
</div>
|
| 374 |
+
<div class="cat-stat">
|
| 375 |
+
<span style="color: #ef4444;">●</span>
|
| 376 |
+
<span>${stats.offline} offline</span>
|
| 377 |
+
</div>
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
`).join('');
|
| 381 |
+
|
| 382 |
+
// Update chart
|
| 383 |
+
updateChart(online, degraded, offline);
|
| 384 |
+
|
| 385 |
+
} catch (error) {
|
| 386 |
+
console.error('Error loading providers:', error);
|
| 387 |
+
document.getElementById('providersGrid').innerHTML = '<div class="loading">Error loading data</div>';
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
function updateChart(online, degraded, offline) {
|
| 392 |
+
const ctx = document.getElementById('statusChart').getContext('2d');
|
| 393 |
+
|
| 394 |
+
if (statusChart) {
|
| 395 |
+
statusChart.destroy();
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
statusChart = new Chart(ctx, {
|
| 399 |
+
type: 'doughnut',
|
| 400 |
+
data: {
|
| 401 |
+
labels: ['Online', 'Degraded', 'Offline'],
|
| 402 |
+
datasets: [{
|
| 403 |
+
data: [online, degraded, offline],
|
| 404 |
+
backgroundColor: ['#10b981', '#f59e0b', '#ef4444'],
|
| 405 |
+
borderWidth: 0
|
| 406 |
+
}]
|
| 407 |
+
},
|
| 408 |
+
options: {
|
| 409 |
+
responsive: true,
|
| 410 |
+
maintainAspectRatio: false,
|
| 411 |
+
plugins: {
|
| 412 |
+
legend: {
|
| 413 |
+
position: 'bottom',
|
| 414 |
+
labels: {
|
| 415 |
+
font: { size: 14 },
|
| 416 |
+
padding: 20
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
});
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
async function refreshAll() {
|
| 425 |
+
const btn = document.querySelector('.refresh-btn');
|
| 426 |
+
btn.classList.add('updating');
|
| 427 |
+
btn.textContent = '⏳ Refreshing...';
|
| 428 |
+
|
| 429 |
+
await loadProviders();
|
| 430 |
+
|
| 431 |
+
btn.classList.remove('updating');
|
| 432 |
+
btn.textContent = '🔄 Refresh Data';
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
// Load on page load
|
| 436 |
+
loadProviders();
|
| 437 |
+
|
| 438 |
+
// Auto-refresh every 30 seconds
|
| 439 |
+
setInterval(loadProviders, 30000);
|
| 440 |
+
</script>
|
| 441 |
+
</body>
|
| 442 |
+
</html>
|
| 443 |
+
|
archive_html/index (1).html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
archive_html/index_backup.html
ADDED
|
@@ -0,0 +1,2452 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto API Monitor - Real-time Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--bg-primary: #ffffff;
|
| 12 |
+
--bg-secondary: #f8f9ff;
|
| 13 |
+
--bg-card: #ffffff;
|
| 14 |
+
--bg-hover: #f3f4ff;
|
| 15 |
+
--text-primary: #1e1b4b;
|
| 16 |
+
--text-secondary: #4c4380;
|
| 17 |
+
--text-muted: #7c3aed;
|
| 18 |
+
--accent-primary: #8b5cf6;
|
| 19 |
+
--accent-secondary: #a78bfa;
|
| 20 |
+
--accent-tertiary: #c084fc;
|
| 21 |
+
--purple-glow: rgba(139, 92, 246, 0.5);
|
| 22 |
+
--success: #10b981;
|
| 23 |
+
--success-bg: #d1fae5;
|
| 24 |
+
--warning: #f59e0b;
|
| 25 |
+
--warning-bg: #fef3c7;
|
| 26 |
+
--danger: #ef4444;
|
| 27 |
+
--danger-bg: #fee2e2;
|
| 28 |
+
--info: #06b6d4;
|
| 29 |
+
--info-bg: #cffafe;
|
| 30 |
+
--border: rgba(139, 92, 246, 0.2);
|
| 31 |
+
--shadow-sm: 0 2px 8px rgba(139, 92, 246, 0.08);
|
| 32 |
+
--shadow: 0 4px 16px rgba(139, 92, 246, 0.12);
|
| 33 |
+
--shadow-lg: 0 10px 40px rgba(139, 92, 246, 0.15);
|
| 34 |
+
--radius: 16px;
|
| 35 |
+
--radius-lg: 24px;
|
| 36 |
+
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
* {
|
| 40 |
+
margin: 0;
|
| 41 |
+
padding: 0;
|
| 42 |
+
box-sizing: border-box;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
body {
|
| 46 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 47 |
+
background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 50%, #f0f4ff 100%);
|
| 48 |
+
background-attachment: fixed;
|
| 49 |
+
color: var(--text-primary);
|
| 50 |
+
line-height: 1.6;
|
| 51 |
+
min-height: 100vh;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
body::before {
|
| 55 |
+
content: '';
|
| 56 |
+
position: fixed;
|
| 57 |
+
top: 0;
|
| 58 |
+
left: 0;
|
| 59 |
+
right: 0;
|
| 60 |
+
bottom: 0;
|
| 61 |
+
background:
|
| 62 |
+
radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.05) 0%, transparent 50%),
|
| 63 |
+
radial-gradient(circle at 80% 70%, rgba(168, 85, 247, 0.05) 0%, transparent 50%);
|
| 64 |
+
pointer-events: none;
|
| 65 |
+
z-index: 0;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.container {
|
| 69 |
+
max-width: 1600px;
|
| 70 |
+
margin: 0 auto;
|
| 71 |
+
padding: 24px;
|
| 72 |
+
position: relative;
|
| 73 |
+
z-index: 1;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Header */
|
| 77 |
+
.header {
|
| 78 |
+
background: var(--bg-card);
|
| 79 |
+
border: 2px solid var(--border);
|
| 80 |
+
border-radius: var(--radius-lg);
|
| 81 |
+
padding: 28px;
|
| 82 |
+
margin-bottom: 24px;
|
| 83 |
+
box-shadow: var(--shadow-lg);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.header-top {
|
| 87 |
+
display: flex;
|
| 88 |
+
align-items: center;
|
| 89 |
+
justify-content: space-between;
|
| 90 |
+
flex-wrap: wrap;
|
| 91 |
+
gap: 20px;
|
| 92 |
+
margin-bottom: 24px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.logo {
|
| 96 |
+
display: flex;
|
| 97 |
+
align-items: center;
|
| 98 |
+
gap: 16px;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.logo-icon {
|
| 102 |
+
width: 64px;
|
| 103 |
+
height: 64px;
|
| 104 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
|
| 105 |
+
border-radius: 20px;
|
| 106 |
+
display: flex;
|
| 107 |
+
align-items: center;
|
| 108 |
+
justify-content: center;
|
| 109 |
+
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.4);
|
| 110 |
+
position: relative;
|
| 111 |
+
overflow: hidden;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.logo-icon::after {
|
| 115 |
+
content: '';
|
| 116 |
+
position: absolute;
|
| 117 |
+
top: -50%;
|
| 118 |
+
left: -50%;
|
| 119 |
+
right: -50%;
|
| 120 |
+
bottom: -50%;
|
| 121 |
+
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 122 |
+
animation: shimmer 3s infinite;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
@keyframes shimmer {
|
| 126 |
+
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
|
| 127 |
+
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.logo-text h1 {
|
| 131 |
+
font-size: 32px;
|
| 132 |
+
font-weight: 900;
|
| 133 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
|
| 134 |
+
-webkit-background-clip: text;
|
| 135 |
+
-webkit-text-fill-color: transparent;
|
| 136 |
+
background-clip: text;
|
| 137 |
+
margin-bottom: 4px;
|
| 138 |
+
letter-spacing: -0.5px;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.logo-text p {
|
| 142 |
+
font-size: 14px;
|
| 143 |
+
color: var(--text-muted);
|
| 144 |
+
font-weight: 600;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.header-actions {
|
| 148 |
+
display: flex;
|
| 149 |
+
gap: 12px;
|
| 150 |
+
align-items: center;
|
| 151 |
+
flex-wrap: wrap;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.status-badge {
|
| 155 |
+
display: flex;
|
| 156 |
+
align-items: center;
|
| 157 |
+
gap: 10px;
|
| 158 |
+
padding: 12px 20px;
|
| 159 |
+
border-radius: 999px;
|
| 160 |
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(16, 185, 129, 0.08) 100%);
|
| 161 |
+
border: 2px solid rgba(16, 185, 129, 0.4);
|
| 162 |
+
font-size: 14px;
|
| 163 |
+
font-weight: 700;
|
| 164 |
+
color: var(--success);
|
| 165 |
+
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
|
| 166 |
+
text-transform: uppercase;
|
| 167 |
+
letter-spacing: 0.5px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.status-dot {
|
| 171 |
+
width: 10px;
|
| 172 |
+
height: 10px;
|
| 173 |
+
border-radius: 50%;
|
| 174 |
+
background: var(--success);
|
| 175 |
+
animation: pulse-glow 2s infinite;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
@keyframes pulse-glow {
|
| 179 |
+
0%, 100% {
|
| 180 |
+
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7),
|
| 181 |
+
0 0 10px rgba(16, 185, 129, 0.5);
|
| 182 |
+
}
|
| 183 |
+
50% {
|
| 184 |
+
box-shadow: 0 0 0 8px rgba(16, 185, 129, 0),
|
| 185 |
+
0 0 20px rgba(16, 185, 129, 0.3);
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.connection-status {
|
| 190 |
+
display: flex;
|
| 191 |
+
align-items: center;
|
| 192 |
+
gap: 8px;
|
| 193 |
+
padding: 10px 16px;
|
| 194 |
+
border-radius: 999px;
|
| 195 |
+
background: var(--bg-card);
|
| 196 |
+
border: 2px solid var(--border);
|
| 197 |
+
font-size: 12px;
|
| 198 |
+
font-weight: 700;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.connection-status.connected { border-color: var(--success); color: var(--success); }
|
| 202 |
+
.connection-status.disconnected { border-color: var(--danger); color: var(--danger); }
|
| 203 |
+
.connection-status.connecting { border-color: var(--warning); color: var(--warning); }
|
| 204 |
+
|
| 205 |
+
.btn {
|
| 206 |
+
padding: 14px 28px;
|
| 207 |
+
border-radius: 14px;
|
| 208 |
+
border: none;
|
| 209 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
|
| 210 |
+
color: white;
|
| 211 |
+
font-family: inherit;
|
| 212 |
+
font-size: 14px;
|
| 213 |
+
font-weight: 700;
|
| 214 |
+
cursor: pointer;
|
| 215 |
+
transition: var(--transition);
|
| 216 |
+
display: inline-flex;
|
| 217 |
+
align-items: center;
|
| 218 |
+
gap: 10px;
|
| 219 |
+
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
|
| 220 |
+
text-transform: uppercase;
|
| 221 |
+
letter-spacing: 0.5px;
|
| 222 |
+
position: relative;
|
| 223 |
+
overflow: hidden;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.btn::before {
|
| 227 |
+
content: '';
|
| 228 |
+
position: absolute;
|
| 229 |
+
top: 0;
|
| 230 |
+
left: -100%;
|
| 231 |
+
width: 100%;
|
| 232 |
+
height: 100%;
|
| 233 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 234 |
+
transition: left 0.5s;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.btn:hover {
|
| 238 |
+
transform: translateY(-3px);
|
| 239 |
+
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.btn:hover::before {
|
| 243 |
+
left: 100%;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.btn-secondary {
|
| 247 |
+
background: white;
|
| 248 |
+
color: var(--accent-primary);
|
| 249 |
+
border: 2px solid var(--border);
|
| 250 |
+
box-shadow: var(--shadow-sm);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.btn-secondary:hover {
|
| 254 |
+
background: var(--bg-hover);
|
| 255 |
+
border-color: var(--accent-primary);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.btn-icon {
|
| 259 |
+
padding: 12px;
|
| 260 |
+
width: 44px;
|
| 261 |
+
height: 44px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.icon {
|
| 265 |
+
width: 20px;
|
| 266 |
+
height: 20px;
|
| 267 |
+
stroke: currentColor;
|
| 268 |
+
stroke-width: 2.5;
|
| 269 |
+
stroke-linecap: round;
|
| 270 |
+
stroke-linejoin: round;
|
| 271 |
+
fill: none;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.icon-lg {
|
| 275 |
+
width: 26px;
|
| 276 |
+
height: 26px;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* KPI Cards */
|
| 280 |
+
.kpi-grid {
|
| 281 |
+
display: grid;
|
| 282 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 283 |
+
gap: 20px;
|
| 284 |
+
margin-bottom: 24px;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
.kpi-card {
|
| 288 |
+
background: var(--bg-card);
|
| 289 |
+
border: 2px solid var(--border);
|
| 290 |
+
border-radius: var(--radius-lg);
|
| 291 |
+
padding: 32px;
|
| 292 |
+
transition: var(--transition);
|
| 293 |
+
box-shadow: var(--shadow);
|
| 294 |
+
position: relative;
|
| 295 |
+
overflow: hidden;
|
| 296 |
+
cursor: pointer;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.kpi-card::before {
|
| 300 |
+
content: '';
|
| 301 |
+
position: absolute;
|
| 302 |
+
top: 0;
|
| 303 |
+
left: 0;
|
| 304 |
+
right: 0;
|
| 305 |
+
height: 6px;
|
| 306 |
+
background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
|
| 307 |
+
transform: scaleX(0);
|
| 308 |
+
transform-origin: left;
|
| 309 |
+
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.kpi-card:hover {
|
| 313 |
+
transform: translateY(-8px) scale(1.02);
|
| 314 |
+
box-shadow: 0 16px 48px rgba(139, 92, 246, 0.25);
|
| 315 |
+
border-color: var(--accent-primary);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.kpi-card:hover::before {
|
| 319 |
+
transform: scaleX(1);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.kpi-header {
|
| 323 |
+
display: flex;
|
| 324 |
+
align-items: center;
|
| 325 |
+
justify-content: space-between;
|
| 326 |
+
margin-bottom: 20px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.kpi-label {
|
| 330 |
+
font-size: 12px;
|
| 331 |
+
color: var(--text-muted);
|
| 332 |
+
font-weight: 800;
|
| 333 |
+
text-transform: uppercase;
|
| 334 |
+
letter-spacing: 1.2px;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.kpi-icon-wrapper {
|
| 338 |
+
width: 64px;
|
| 339 |
+
height: 64px;
|
| 340 |
+
border-radius: 18px;
|
| 341 |
+
display: flex;
|
| 342 |
+
align-items: center;
|
| 343 |
+
justify-content: center;
|
| 344 |
+
transition: var(--transition);
|
| 345 |
+
box-shadow: var(--shadow);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.kpi-card:hover .kpi-icon-wrapper {
|
| 349 |
+
transform: rotate(-5deg) scale(1.15);
|
| 350 |
+
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.3);
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.kpi-value {
|
| 354 |
+
font-size: 48px;
|
| 355 |
+
font-weight: 900;
|
| 356 |
+
margin-bottom: 16px;
|
| 357 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
|
| 358 |
+
-webkit-background-clip: text;
|
| 359 |
+
-webkit-text-fill-color: transparent;
|
| 360 |
+
background-clip: text;
|
| 361 |
+
line-height: 1;
|
| 362 |
+
animation: countUp 0.6s ease-out;
|
| 363 |
+
letter-spacing: -2px;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
@keyframes countUp {
|
| 367 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 368 |
+
to { opacity: 1; transform: translateY(0); }
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.kpi-trend {
|
| 372 |
+
display: flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
gap: 10px;
|
| 375 |
+
font-size: 13px;
|
| 376 |
+
font-weight: 700;
|
| 377 |
+
padding: 8px 16px;
|
| 378 |
+
border-radius: 12px;
|
| 379 |
+
width: fit-content;
|
| 380 |
+
text-transform: uppercase;
|
| 381 |
+
letter-spacing: 0.5px;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.trend-up {
|
| 385 |
+
color: var(--success);
|
| 386 |
+
background: var(--success-bg);
|
| 387 |
+
border: 2px solid var(--success);
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.trend-down {
|
| 391 |
+
color: var(--danger);
|
| 392 |
+
background: var(--danger-bg);
|
| 393 |
+
border: 2px solid var(--danger);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.trend-neutral {
|
| 397 |
+
color: var(--info);
|
| 398 |
+
background: var(--info-bg);
|
| 399 |
+
border: 2px solid var(--info);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
/* Tabs */
|
| 403 |
+
.tabs {
|
| 404 |
+
display: flex;
|
| 405 |
+
gap: 6px;
|
| 406 |
+
margin-bottom: 24px;
|
| 407 |
+
overflow-x: auto;
|
| 408 |
+
padding: 8px;
|
| 409 |
+
background: var(--bg-card);
|
| 410 |
+
border-radius: var(--radius-lg);
|
| 411 |
+
border: 2px solid var(--border);
|
| 412 |
+
box-shadow: var(--shadow-sm);
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.tab {
|
| 416 |
+
padding: 12px 20px;
|
| 417 |
+
border-radius: 12px;
|
| 418 |
+
background: transparent;
|
| 419 |
+
border: none;
|
| 420 |
+
color: var(--text-secondary);
|
| 421 |
+
cursor: pointer;
|
| 422 |
+
transition: all 0.25s;
|
| 423 |
+
white-space: nowrap;
|
| 424 |
+
font-weight: 700;
|
| 425 |
+
font-size: 13px;
|
| 426 |
+
display: flex;
|
| 427 |
+
align-items: center;
|
| 428 |
+
gap: 8px;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.tab:hover:not(.active) {
|
| 432 |
+
background: var(--bg-hover);
|
| 433 |
+
color: var(--text-primary);
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.tab.active {
|
| 437 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
|
| 438 |
+
color: white;
|
| 439 |
+
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
|
| 440 |
+
transform: scale(1.05);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.tab .icon {
|
| 444 |
+
width: 16px;
|
| 445 |
+
height: 16px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* Tab Content */
|
| 449 |
+
.tab-content {
|
| 450 |
+
display: none;
|
| 451 |
+
animation: fadeIn 0.4s ease;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.tab-content.active {
|
| 455 |
+
display: block;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
@keyframes fadeIn {
|
| 459 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 460 |
+
to { opacity: 1; transform: translateY(0); }
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
/* Card */
|
| 464 |
+
.card {
|
| 465 |
+
background: var(--bg-card);
|
| 466 |
+
border: 2px solid var(--border);
|
| 467 |
+
border-radius: var(--radius-lg);
|
| 468 |
+
padding: 28px;
|
| 469 |
+
margin-bottom: 24px;
|
| 470 |
+
box-shadow: var(--shadow);
|
| 471 |
+
transition: var(--transition);
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.card:hover {
|
| 475 |
+
box-shadow: var(--shadow-lg);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.card-header {
|
| 479 |
+
display: flex;
|
| 480 |
+
align-items: center;
|
| 481 |
+
justify-content: space-between;
|
| 482 |
+
margin-bottom: 24px;
|
| 483 |
+
padding-bottom: 16px;
|
| 484 |
+
border-bottom: 2px solid var(--border);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.card-title {
|
| 488 |
+
font-size: 20px;
|
| 489 |
+
font-weight: 800;
|
| 490 |
+
display: flex;
|
| 491 |
+
align-items: center;
|
| 492 |
+
gap: 12px;
|
| 493 |
+
color: var(--text-primary);
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
.card-actions {
|
| 497 |
+
display: flex;
|
| 498 |
+
gap: 8px;
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
/* Table */
|
| 502 |
+
.table-container {
|
| 503 |
+
overflow-x: auto;
|
| 504 |
+
border-radius: var(--radius);
|
| 505 |
+
border: 2px solid var(--border);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.table {
|
| 509 |
+
width: 100%;
|
| 510 |
+
border-collapse: collapse;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.table thead {
|
| 514 |
+
background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.table thead th {
|
| 518 |
+
color: white;
|
| 519 |
+
font-weight: 700;
|
| 520 |
+
font-size: 13px;
|
| 521 |
+
text-align: left;
|
| 522 |
+
padding: 16px;
|
| 523 |
+
text-transform: uppercase;
|
| 524 |
+
letter-spacing: 0.8px;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.table tbody tr {
|
| 528 |
+
transition: var(--transition);
|
| 529 |
+
border-bottom: 1px solid var(--border);
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.table tbody tr:hover {
|
| 533 |
+
background: var(--bg-hover);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.table tbody td {
|
| 537 |
+
padding: 16px;
|
| 538 |
+
font-size: 14px;
|
| 539 |
+
color: var(--text-secondary);
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
/* Badge */
|
| 543 |
+
.badge {
|
| 544 |
+
display: inline-flex;
|
| 545 |
+
align-items: center;
|
| 546 |
+
gap: 6px;
|
| 547 |
+
padding: 6px 12px;
|
| 548 |
+
border-radius: 999px;
|
| 549 |
+
font-size: 12px;
|
| 550 |
+
font-weight: 700;
|
| 551 |
+
white-space: nowrap;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.badge-success {
|
| 555 |
+
background: var(--success-bg);
|
| 556 |
+
color: var(--success);
|
| 557 |
+
border: 2px solid var(--success);
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
.badge-warning {
|
| 561 |
+
background: var(--warning-bg);
|
| 562 |
+
color: var(--warning);
|
| 563 |
+
border: 2px solid var(--warning);
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.badge-danger {
|
| 567 |
+
background: var(--danger-bg);
|
| 568 |
+
color: var(--danger);
|
| 569 |
+
border: 2px solid var(--danger);
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.badge-info {
|
| 573 |
+
background: var(--info-bg);
|
| 574 |
+
color: var(--info);
|
| 575 |
+
border: 2px solid var(--info);
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
/* Progress Bar */
|
| 579 |
+
.progress {
|
| 580 |
+
height: 12px;
|
| 581 |
+
background: var(--bg-hover);
|
| 582 |
+
border-radius: 999px;
|
| 583 |
+
overflow: hidden;
|
| 584 |
+
margin: 8px 0;
|
| 585 |
+
border: 2px solid var(--border);
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.progress-bar {
|
| 589 |
+
height: 100%;
|
| 590 |
+
background: linear-gradient(90deg, #8b5cf6, #a78bfa);
|
| 591 |
+
border-radius: 999px;
|
| 592 |
+
transition: width 0.5s ease;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.progress-bar.success {
|
| 596 |
+
background: linear-gradient(90deg, var(--success), #34d399);
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.progress-bar.warning {
|
| 600 |
+
background: linear-gradient(90deg, var(--warning), #fbbf24);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.progress-bar.danger {
|
| 604 |
+
background: linear-gradient(90deg, var(--danger), #f87171);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
/* Chart Container */
|
| 608 |
+
.chart-container {
|
| 609 |
+
position: relative;
|
| 610 |
+
height: 320px;
|
| 611 |
+
margin: 20px 0;
|
| 612 |
+
background: var(--bg-secondary);
|
| 613 |
+
border-radius: var(--radius);
|
| 614 |
+
padding: 16px;
|
| 615 |
+
border: 2px solid var(--border);
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
/* Loading */
|
| 619 |
+
.loading-overlay {
|
| 620 |
+
position: fixed;
|
| 621 |
+
top: 0;
|
| 622 |
+
left: 0;
|
| 623 |
+
right: 0;
|
| 624 |
+
bottom: 0;
|
| 625 |
+
background: rgba(255, 255, 255, 0.95);
|
| 626 |
+
backdrop-filter: blur(8px);
|
| 627 |
+
display: none;
|
| 628 |
+
align-items: center;
|
| 629 |
+
justify-content: center;
|
| 630 |
+
z-index: 9999;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.loading-overlay.active {
|
| 634 |
+
display: flex;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.spinner {
|
| 638 |
+
width: 60px;
|
| 639 |
+
height: 60px;
|
| 640 |
+
border: 6px solid var(--border);
|
| 641 |
+
border-top-color: var(--accent-primary);
|
| 642 |
+
border-radius: 50%;
|
| 643 |
+
animation: spin 0.8s linear infinite;
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
@keyframes spin {
|
| 647 |
+
to { transform: rotate(360deg); }
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.loading-inline {
|
| 651 |
+
display: flex;
|
| 652 |
+
align-items: center;
|
| 653 |
+
justify-content: center;
|
| 654 |
+
padding: 40px;
|
| 655 |
+
color: var(--text-muted);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.spinner-inline {
|
| 659 |
+
width: 32px;
|
| 660 |
+
height: 32px;
|
| 661 |
+
border: 3px solid var(--border);
|
| 662 |
+
border-top-color: var(--accent-primary);
|
| 663 |
+
border-radius: 50%;
|
| 664 |
+
animation: spin 0.8s linear infinite;
|
| 665 |
+
margin-right: 12px;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
/* Toast */
|
| 669 |
+
.toast-container {
|
| 670 |
+
position: fixed;
|
| 671 |
+
bottom: 24px;
|
| 672 |
+
right: 24px;
|
| 673 |
+
z-index: 10000;
|
| 674 |
+
display: flex;
|
| 675 |
+
flex-direction: column;
|
| 676 |
+
gap: 12px;
|
| 677 |
+
max-width: 400px;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.toast {
|
| 681 |
+
padding: 16px 20px;
|
| 682 |
+
border-radius: var(--radius);
|
| 683 |
+
background: var(--bg-card);
|
| 684 |
+
border: 2px solid var(--border);
|
| 685 |
+
box-shadow: var(--shadow-lg);
|
| 686 |
+
display: flex;
|
| 687 |
+
align-items: center;
|
| 688 |
+
gap: 12px;
|
| 689 |
+
animation: slideInRight 0.3s ease;
|
| 690 |
+
min-width: 300px;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
@keyframes slideInRight {
|
| 694 |
+
from { transform: translateX(400px); opacity: 0; }
|
| 695 |
+
to { transform: translateX(0); opacity: 1; }
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.toast.success { border-color: var(--success); background: var(--success-bg); }
|
| 699 |
+
.toast.error { border-color: var(--danger); background: var(--danger-bg); }
|
| 700 |
+
.toast.warning { border-color: var(--warning); background: var(--warning-bg); }
|
| 701 |
+
.toast.info { border-color: var(--info); background: var(--info-bg); }
|
| 702 |
+
|
| 703 |
+
.toast-content { flex: 1; }
|
| 704 |
+
.toast-title { font-weight: 700; font-size: 14px; margin-bottom: 2px; }
|
| 705 |
+
.toast-message { font-size: 13px; color: var(--text-secondary); }
|
| 706 |
+
|
| 707 |
+
/* Alert */
|
| 708 |
+
.alert {
|
| 709 |
+
padding: 18px 24px;
|
| 710 |
+
border-radius: var(--radius);
|
| 711 |
+
margin-bottom: 16px;
|
| 712 |
+
display: flex;
|
| 713 |
+
align-items: flex-start;
|
| 714 |
+
gap: 14px;
|
| 715 |
+
border-left: 6px solid;
|
| 716 |
+
box-shadow: var(--shadow-sm);
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.alert-success { background: var(--success-bg); border-color: var(--success); color: var(--success); }
|
| 720 |
+
.alert-warning { background: var(--warning-bg); border-color: var(--warning); color: var(--warning); }
|
| 721 |
+
.alert-danger { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
|
| 722 |
+
.alert-info { background: var(--info-bg); border-color: var(--info); color: var(--info); }
|
| 723 |
+
|
| 724 |
+
.alert-content { flex: 1; }
|
| 725 |
+
.alert-title { font-weight: 800; margin-bottom: 6px; font-size: 15px; }
|
| 726 |
+
.alert-message { font-size: 14px; opacity: 0.9; }
|
| 727 |
+
|
| 728 |
+
/* Grid */
|
| 729 |
+
.grid { display: grid; gap: 20px; }
|
| 730 |
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
|
| 731 |
+
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
|
| 732 |
+
|
| 733 |
+
/* Input */
|
| 734 |
+
.input {
|
| 735 |
+
width: 100%;
|
| 736 |
+
padding: 12px 16px;
|
| 737 |
+
border-radius: var(--radius);
|
| 738 |
+
border: 2px solid var(--border);
|
| 739 |
+
background: var(--bg-card);
|
| 740 |
+
color: var(--text-primary);
|
| 741 |
+
font-family: inherit;
|
| 742 |
+
font-size: 14px;
|
| 743 |
+
transition: var(--transition);
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.input:focus {
|
| 747 |
+
outline: none;
|
| 748 |
+
border-color: var(--accent-primary);
|
| 749 |
+
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
/* Responsive */
|
| 753 |
+
@media (max-width: 768px) {
|
| 754 |
+
.container { padding: 16px; }
|
| 755 |
+
.header-top { flex-direction: column; align-items: flex-start; }
|
| 756 |
+
.kpi-grid { grid-template-columns: 1fr; }
|
| 757 |
+
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
| 758 |
+
.card-header { flex-direction: column; align-items: flex-start; gap: 16px; }
|
| 759 |
+
.card-actions { width: 100%; justify-content: flex-end; }
|
| 760 |
+
}
|
| 761 |
+
</style>
|
| 762 |
+
</head>
|
| 763 |
+
<body>
|
| 764 |
+
<div class="loading-overlay" id="loadingOverlay">
|
| 765 |
+
<div class="spinner"></div>
|
| 766 |
+
</div>
|
| 767 |
+
|
| 768 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 769 |
+
|
| 770 |
+
<div class="container">
|
| 771 |
+
<div class="header">
|
| 772 |
+
<div class="header-top">
|
| 773 |
+
<div class="logo">
|
| 774 |
+
<div class="logo-icon">
|
| 775 |
+
<svg class="icon icon-lg" style="stroke: white;">
|
| 776 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 777 |
+
<path d="M12 6v6l4 2"></path>
|
| 778 |
+
</svg>
|
| 779 |
+
</div>
|
| 780 |
+
<div class="logo-text">
|
| 781 |
+
<h1>Crypto API Monitor</h1>
|
| 782 |
+
<p>Real-time Cryptocurrency API Resource Monitoring</p>
|
| 783 |
+
</div>
|
| 784 |
+
</div>
|
| 785 |
+
<div class="header-actions">
|
| 786 |
+
<div class="connection-status" id="wsStatus">
|
| 787 |
+
<span class="status-dot"></span>
|
| 788 |
+
<span id="wsStatusText">Connecting...</span>
|
| 789 |
+
</div>
|
| 790 |
+
<div class="status-badge" id="systemStatus">
|
| 791 |
+
<span class="status-dot"></span>
|
| 792 |
+
<span id="systemStatusText">System Active</span>
|
| 793 |
+
</div>
|
| 794 |
+
<button class="btn" onclick="refreshAll()">
|
| 795 |
+
<svg class="icon">
|
| 796 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 797 |
+
</svg>
|
| 798 |
+
Refresh All
|
| 799 |
+
</button>
|
| 800 |
+
</div>
|
| 801 |
+
</div>
|
| 802 |
+
|
| 803 |
+
<div class="kpi-grid" id="kpiGrid">
|
| 804 |
+
<div class="kpi-card">
|
| 805 |
+
<div class="kpi-header">
|
| 806 |
+
<span class="kpi-label">Total APIs</span>
|
| 807 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.1) 100%);">
|
| 808 |
+
<svg class="icon icon-lg" style="stroke: #3b82f6;">
|
| 809 |
+
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
| 810 |
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
| 811 |
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
| 812 |
+
</svg>
|
| 813 |
+
</div>
|
| 814 |
+
</div>
|
| 815 |
+
<div class="kpi-value" id="kpiTotalAPIs">--</div>
|
| 816 |
+
<div class="kpi-trend trend-neutral">
|
| 817 |
+
<svg class="icon" style="width: 16px; height: 16px;">
|
| 818 |
+
<path d="M12 20V10M18 20V4M6 20v-4"></path>
|
| 819 |
+
</svg>
|
| 820 |
+
<span id="kpiTotalTrend">Loading...</span>
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
|
| 824 |
+
<div class="kpi-card">
|
| 825 |
+
<div class="kpi-header">
|
| 826 |
+
<span class="kpi-label">Online</span>
|
| 827 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.1) 100%);">
|
| 828 |
+
<svg class="icon icon-lg" style="stroke: #10b981;">
|
| 829 |
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
| 830 |
+
<polyline points="9 12 11 14 15 10"></polyline>
|
| 831 |
+
</svg>
|
| 832 |
+
</div>
|
| 833 |
+
</div>
|
| 834 |
+
<div class="kpi-value" id="kpiOnline">--</div>
|
| 835 |
+
<div class="kpi-trend trend-up">
|
| 836 |
+
<svg class="icon" style="width: 16px; height: 16px;">
|
| 837 |
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
| 838 |
+
<polyline points="5 12 12 5 19 12"></polyline>
|
| 839 |
+
</svg>
|
| 840 |
+
<span id="kpiOnlineTrend">Loading...</span>
|
| 841 |
+
</div>
|
| 842 |
+
</div>
|
| 843 |
+
|
| 844 |
+
<div class="kpi-card">
|
| 845 |
+
<div class="kpi-header">
|
| 846 |
+
<span class="kpi-label">Avg Response</span>
|
| 847 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(217, 119, 6, 0.1) 100%);">
|
| 848 |
+
<svg class="icon icon-lg" style="stroke: #f59e0b;">
|
| 849 |
+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
| 850 |
+
</svg>
|
| 851 |
+
</div>
|
| 852 |
+
</div>
|
| 853 |
+
<div class="kpi-value" id="kpiAvgResponse" style="font-size: 32px;">--</div>
|
| 854 |
+
<div class="kpi-trend trend-down">
|
| 855 |
+
<svg class="icon" style="width: 16px; height: 16px;">
|
| 856 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 857 |
+
<polyline points="19 12 12 19 5 12"></polyline>
|
| 858 |
+
</svg>
|
| 859 |
+
<span id="kpiResponseTrend">Loading...</span>
|
| 860 |
+
</div>
|
| 861 |
+
</div>
|
| 862 |
+
|
| 863 |
+
<div class="kpi-card">
|
| 864 |
+
<div class="kpi-header">
|
| 865 |
+
<span class="kpi-label">Last Update</span>
|
| 866 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(124, 58, 237, 0.1) 100%);">
|
| 867 |
+
<svg class="icon icon-lg" style="stroke: #8b5cf6;">
|
| 868 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 869 |
+
<polyline points="12 6 12 12 16 14"></polyline>
|
| 870 |
+
</svg>
|
| 871 |
+
</div>
|
| 872 |
+
</div>
|
| 873 |
+
<div class="kpi-value" id="kpiLastUpdate" style="font-size: 20px; line-height: 1.2;">--</div>
|
| 874 |
+
<div class="kpi-trend trend-neutral">
|
| 875 |
+
<svg class="icon" style="width: 16px; height: 16px;">
|
| 876 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 877 |
+
</svg>
|
| 878 |
+
<span>Auto-refresh enabled</span>
|
| 879 |
+
</div>
|
| 880 |
+
</div>
|
| 881 |
+
</div>
|
| 882 |
+
</div>
|
| 883 |
+
|
| 884 |
+
<div class="tabs">
|
| 885 |
+
<div class="tab active" onclick="switchTab(event, 'dashboard')">
|
| 886 |
+
<svg class="icon">
|
| 887 |
+
<rect x="3" y="3" width="7" height="7"></rect>
|
| 888 |
+
<rect x="14" y="3" width="7" height="7"></rect>
|
| 889 |
+
<rect x="14" y="14" width="7" height="7"></rect>
|
| 890 |
+
<rect x="3" y="14" width="7" height="7"></rect>
|
| 891 |
+
</svg>
|
| 892 |
+
<span>Dashboard</span>
|
| 893 |
+
</div>
|
| 894 |
+
<div class="tab" onclick="switchTab(event, 'providers')">
|
| 895 |
+
<svg class="icon">
|
| 896 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 897 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 898 |
+
</svg>
|
| 899 |
+
<span>Providers</span>
|
| 900 |
+
</div>
|
| 901 |
+
<div class="tab" onclick="switchTab(event, 'categories')">
|
| 902 |
+
<svg class="icon">
|
| 903 |
+
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
| 904 |
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
| 905 |
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
| 906 |
+
</svg>
|
| 907 |
+
<span>Categories</span>
|
| 908 |
+
</div>
|
| 909 |
+
<div class="tab" onclick="switchTab(event, 'ratelimits')">
|
| 910 |
+
<svg class="icon">
|
| 911 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 912 |
+
<polyline points="12 6 12 12 16 14"></polyline>
|
| 913 |
+
</svg>
|
| 914 |
+
<span>Rate Limits</span>
|
| 915 |
+
</div>
|
| 916 |
+
<div class="tab" onclick="switchTab(event, 'logs')">
|
| 917 |
+
<svg class="icon">
|
| 918 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 919 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 920 |
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
| 921 |
+
</svg>
|
| 922 |
+
<span>Logs</span>
|
| 923 |
+
</div>
|
| 924 |
+
<div class="tab" onclick="switchTab(event, 'alerts')">
|
| 925 |
+
<svg class="icon">
|
| 926 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 927 |
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
| 928 |
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
| 929 |
+
</svg>
|
| 930 |
+
<span>Alerts</span>
|
| 931 |
+
</div>
|
| 932 |
+
<div class="tab" onclick="switchTab(event, 'huggingface')">
|
| 933 |
+
<svg class="icon">
|
| 934 |
+
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
|
| 935 |
+
<circle cx="12" cy="12" r="3"></circle>
|
| 936 |
+
</svg>
|
| 937 |
+
<span>HuggingFace</span>
|
| 938 |
+
</div>
|
| 939 |
+
</div>
|
| 940 |
+
|
| 941 |
+
<!-- Dashboard Tab -->
|
| 942 |
+
<div class="tab-content active" id="tab-dashboard">
|
| 943 |
+
<div id="alertsContainer"></div>
|
| 944 |
+
|
| 945 |
+
<div class="card">
|
| 946 |
+
<div class="card-header">
|
| 947 |
+
<h2 class="card-title">
|
| 948 |
+
<svg class="icon icon-lg">
|
| 949 |
+
<rect x="3" y="3" width="7" height="7"></rect>
|
| 950 |
+
<rect x="14" y="3" width="7" height="7"></rect>
|
| 951 |
+
</svg>
|
| 952 |
+
System Overview
|
| 953 |
+
</h2>
|
| 954 |
+
<button class="btn btn-secondary" onclick="loadProviders()">
|
| 955 |
+
<svg class="icon">
|
| 956 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 957 |
+
</svg>
|
| 958 |
+
Refresh
|
| 959 |
+
</button>
|
| 960 |
+
</div>
|
| 961 |
+
<div class="table-container">
|
| 962 |
+
<table class="table">
|
| 963 |
+
<thead>
|
| 964 |
+
<tr>
|
| 965 |
+
<th>Provider</th>
|
| 966 |
+
<th>Category</th>
|
| 967 |
+
<th>Status</th>
|
| 968 |
+
<th>Response Time</th>
|
| 969 |
+
<th>Last Check</th>
|
| 970 |
+
</tr>
|
| 971 |
+
</thead>
|
| 972 |
+
<tbody id="providersTableBody">
|
| 973 |
+
<tr>
|
| 974 |
+
<td colspan="5">
|
| 975 |
+
<div class="loading-inline">
|
| 976 |
+
<div class="spinner-inline"></div>
|
| 977 |
+
Loading providers...
|
| 978 |
+
</div>
|
| 979 |
+
</td>
|
| 980 |
+
</tr>
|
| 981 |
+
</tbody>
|
| 982 |
+
</table>
|
| 983 |
+
</div>
|
| 984 |
+
</div>
|
| 985 |
+
|
| 986 |
+
<div class="grid grid-2">
|
| 987 |
+
<div class="card">
|
| 988 |
+
<div class="card-header">
|
| 989 |
+
<h2 class="card-title">
|
| 990 |
+
<svg class="icon icon-lg">
|
| 991 |
+
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
|
| 992 |
+
</svg>
|
| 993 |
+
Health Status
|
| 994 |
+
</h2>
|
| 995 |
+
</div>
|
| 996 |
+
<div class="chart-container">
|
| 997 |
+
<canvas id="healthChart"></canvas>
|
| 998 |
+
</div>
|
| 999 |
+
</div>
|
| 1000 |
+
|
| 1001 |
+
<div class="card">
|
| 1002 |
+
<div class="card-header">
|
| 1003 |
+
<h2 class="card-title">
|
| 1004 |
+
<svg class="icon icon-lg">
|
| 1005 |
+
<path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path>
|
| 1006 |
+
</svg>
|
| 1007 |
+
Status Distribution
|
| 1008 |
+
</h2>
|
| 1009 |
+
</div>
|
| 1010 |
+
<div class="chart-container">
|
| 1011 |
+
<canvas id="statusChart"></canvas>
|
| 1012 |
+
</div>
|
| 1013 |
+
</div>
|
| 1014 |
+
</div>
|
| 1015 |
+
</div>
|
| 1016 |
+
|
| 1017 |
+
<!-- Providers Tab -->
|
| 1018 |
+
<div class="tab-content" id="tab-providers">
|
| 1019 |
+
<div class="card">
|
| 1020 |
+
<div class="card-header">
|
| 1021 |
+
<h2 class="card-title">
|
| 1022 |
+
<svg class="icon icon-lg">
|
| 1023 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1024 |
+
</svg>
|
| 1025 |
+
All Providers
|
| 1026 |
+
</h2>
|
| 1027 |
+
<button class="btn btn-secondary" onclick="loadProviders()">
|
| 1028 |
+
<svg class="icon">
|
| 1029 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1030 |
+
</svg>
|
| 1031 |
+
Refresh
|
| 1032 |
+
</button>
|
| 1033 |
+
</div>
|
| 1034 |
+
<div id="providersDetail">
|
| 1035 |
+
<div class="loading-inline">
|
| 1036 |
+
<div class="spinner-inline"></div>
|
| 1037 |
+
Loading providers details...
|
| 1038 |
+
</div>
|
| 1039 |
+
</div>
|
| 1040 |
+
</div>
|
| 1041 |
+
</div>
|
| 1042 |
+
|
| 1043 |
+
<!-- Categories Tab -->
|
| 1044 |
+
<div class="tab-content" id="tab-categories">
|
| 1045 |
+
<div class="card">
|
| 1046 |
+
<div class="card-header">
|
| 1047 |
+
<h2 class="card-title">
|
| 1048 |
+
<svg class="icon icon-lg">
|
| 1049 |
+
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
| 1050 |
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
| 1051 |
+
</svg>
|
| 1052 |
+
Categories Overview
|
| 1053 |
+
</h2>
|
| 1054 |
+
<button class="btn btn-secondary" onclick="loadCategories()">
|
| 1055 |
+
<svg class="icon">
|
| 1056 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1057 |
+
</svg>
|
| 1058 |
+
Refresh
|
| 1059 |
+
</button>
|
| 1060 |
+
</div>
|
| 1061 |
+
<div class="table-container">
|
| 1062 |
+
<table class="table">
|
| 1063 |
+
<thead>
|
| 1064 |
+
<tr>
|
| 1065 |
+
<th>Category</th>
|
| 1066 |
+
<th>Total Sources</th>
|
| 1067 |
+
<th>Online</th>
|
| 1068 |
+
<th>Health %</th>
|
| 1069 |
+
<th>Avg Response</th>
|
| 1070 |
+
<th>Last Updated</th>
|
| 1071 |
+
<th>Status</th>
|
| 1072 |
+
</tr>
|
| 1073 |
+
</thead>
|
| 1074 |
+
<tbody id="categoriesTableBody">
|
| 1075 |
+
<tr>
|
| 1076 |
+
<td colspan="7">
|
| 1077 |
+
<div class="loading-inline">
|
| 1078 |
+
<div class="spinner-inline"></div>
|
| 1079 |
+
Loading categories...
|
| 1080 |
+
</div>
|
| 1081 |
+
</td>
|
| 1082 |
+
</tr>
|
| 1083 |
+
</tbody>
|
| 1084 |
+
</table>
|
| 1085 |
+
</div>
|
| 1086 |
+
</div>
|
| 1087 |
+
</div>
|
| 1088 |
+
|
| 1089 |
+
<!-- Rate Limits Tab -->
|
| 1090 |
+
<div class="tab-content" id="tab-ratelimits">
|
| 1091 |
+
<div class="card">
|
| 1092 |
+
<div class="card-header">
|
| 1093 |
+
<h2 class="card-title">
|
| 1094 |
+
<svg class="icon icon-lg">
|
| 1095 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1096 |
+
<polyline points="12 6 12 12 16 14"></polyline>
|
| 1097 |
+
</svg>
|
| 1098 |
+
Rate Limit Monitor
|
| 1099 |
+
</h2>
|
| 1100 |
+
<button class="btn btn-secondary" onclick="loadRateLimits()">
|
| 1101 |
+
<svg class="icon">
|
| 1102 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1103 |
+
</svg>
|
| 1104 |
+
Refresh
|
| 1105 |
+
</button>
|
| 1106 |
+
</div>
|
| 1107 |
+
<div id="rateLimitCards" class="grid grid-2">
|
| 1108 |
+
<div class="loading-inline">
|
| 1109 |
+
<div class="spinner-inline"></div>
|
| 1110 |
+
Loading rate limits...
|
| 1111 |
+
</div>
|
| 1112 |
+
</div>
|
| 1113 |
+
</div>
|
| 1114 |
+
</div>
|
| 1115 |
+
|
| 1116 |
+
<!-- Logs Tab -->
|
| 1117 |
+
<div class="tab-content" id="tab-logs">
|
| 1118 |
+
<div class="card">
|
| 1119 |
+
<div class="card-header">
|
| 1120 |
+
<h2 class="card-title">
|
| 1121 |
+
<svg class="icon icon-lg">
|
| 1122 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 1123 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 1124 |
+
</svg>
|
| 1125 |
+
Connection Logs
|
| 1126 |
+
</h2>
|
| 1127 |
+
<div class="card-actions">
|
| 1128 |
+
<select id="logType" class="input" style="width: auto; padding: 10px 16px;" onchange="loadLogs()">
|
| 1129 |
+
<option value="connection">Connection</option>
|
| 1130 |
+
<option value="error">Error</option>
|
| 1131 |
+
<option value="rate_limit">Rate Limit</option>
|
| 1132 |
+
<option value="all">All</option>
|
| 1133 |
+
</select>
|
| 1134 |
+
<button class="btn btn-secondary" onclick="loadLogs()">
|
| 1135 |
+
<svg class="icon">
|
| 1136 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1137 |
+
</svg>
|
| 1138 |
+
Refresh
|
| 1139 |
+
</button>
|
| 1140 |
+
</div>
|
| 1141 |
+
</div>
|
| 1142 |
+
<div class="table-container">
|
| 1143 |
+
<table class="table">
|
| 1144 |
+
<thead>
|
| 1145 |
+
<tr>
|
| 1146 |
+
<th>Timestamp</th>
|
| 1147 |
+
<th>Provider</th>
|
| 1148 |
+
<th>Type</th>
|
| 1149 |
+
<th>Status</th>
|
| 1150 |
+
<th>Response Time</th>
|
| 1151 |
+
<th>Message</th>
|
| 1152 |
+
</tr>
|
| 1153 |
+
</thead>
|
| 1154 |
+
<tbody id="logsTableBody">
|
| 1155 |
+
<tr>
|
| 1156 |
+
<td colspan="6">
|
| 1157 |
+
<div class="loading-inline">
|
| 1158 |
+
<div class="spinner-inline"></div>
|
| 1159 |
+
Loading logs...
|
| 1160 |
+
</div>
|
| 1161 |
+
</td>
|
| 1162 |
+
</tr>
|
| 1163 |
+
</tbody>
|
| 1164 |
+
</table>
|
| 1165 |
+
</div>
|
| 1166 |
+
</div>
|
| 1167 |
+
</div>
|
| 1168 |
+
|
| 1169 |
+
<!-- Alerts Tab -->
|
| 1170 |
+
<div class="tab-content" id="tab-alerts">
|
| 1171 |
+
<div class="card">
|
| 1172 |
+
<div class="card-header">
|
| 1173 |
+
<h2 class="card-title">
|
| 1174 |
+
<svg class="icon icon-lg">
|
| 1175 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1176 |
+
<line x1="12" y1="8" x2="12" y2="12"></line>
|
| 1177 |
+
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
| 1178 |
+
</svg>
|
| 1179 |
+
System Alerts
|
| 1180 |
+
</h2>
|
| 1181 |
+
<button class="btn btn-secondary" onclick="loadAlerts()">
|
| 1182 |
+
<svg class="icon">
|
| 1183 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1184 |
+
</svg>
|
| 1185 |
+
Refresh
|
| 1186 |
+
</button>
|
| 1187 |
+
</div>
|
| 1188 |
+
<div id="alertsList">
|
| 1189 |
+
<div class="loading-inline">
|
| 1190 |
+
<div class="spinner-inline"></div>
|
| 1191 |
+
Loading alerts...
|
| 1192 |
+
</div>
|
| 1193 |
+
</div>
|
| 1194 |
+
</div>
|
| 1195 |
+
</div>
|
| 1196 |
+
|
| 1197 |
+
<!-- HuggingFace Tab -->
|
| 1198 |
+
<div class="tab-content" id="tab-huggingface">
|
| 1199 |
+
<div class="card">
|
| 1200 |
+
<div class="card-header">
|
| 1201 |
+
<h2 class="card-title">
|
| 1202 |
+
<svg class="icon icon-lg">
|
| 1203 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1204 |
+
</svg>
|
| 1205 |
+
🤗 HuggingFace Health Status
|
| 1206 |
+
</h2>
|
| 1207 |
+
<div class="card-actions">
|
| 1208 |
+
<button class="btn" onclick="refreshHFRegistry()">
|
| 1209 |
+
<svg class="icon">
|
| 1210 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1211 |
+
</svg>
|
| 1212 |
+
Refresh Registry
|
| 1213 |
+
</button>
|
| 1214 |
+
</div>
|
| 1215 |
+
</div>
|
| 1216 |
+
<div id="hfHealthDisplay" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; border: 2px solid var(--border);">
|
| 1217 |
+
Loading HF health status...
|
| 1218 |
+
</div>
|
| 1219 |
+
</div>
|
| 1220 |
+
|
| 1221 |
+
<div class="grid grid-2">
|
| 1222 |
+
<div class="card">
|
| 1223 |
+
<div class="card-header">
|
| 1224 |
+
<h2 class="card-title">
|
| 1225 |
+
Models Registry
|
| 1226 |
+
<span class="badge badge-success" id="hfModelsCount">0</span>
|
| 1227 |
+
</h2>
|
| 1228 |
+
</div>
|
| 1229 |
+
<div id="hfModelsList" style="max-height: 400px; overflow-y: auto;">
|
| 1230 |
+
<div class="loading-inline">
|
| 1231 |
+
<div class="spinner-inline"></div>
|
| 1232 |
+
Loading models...
|
| 1233 |
+
</div>
|
| 1234 |
+
</div>
|
| 1235 |
+
</div>
|
| 1236 |
+
|
| 1237 |
+
<div class="card">
|
| 1238 |
+
<div class="card-header">
|
| 1239 |
+
<h2 class="card-title">
|
| 1240 |
+
Datasets Registry
|
| 1241 |
+
<span class="badge badge-success" id="hfDatasetsCount">0</span>
|
| 1242 |
+
</h2>
|
| 1243 |
+
</div>
|
| 1244 |
+
<div id="hfDatasetsList" style="max-height: 400px; overflow-y: auto;">
|
| 1245 |
+
<div class="loading-inline">
|
| 1246 |
+
<div class="spinner-inline"></div>
|
| 1247 |
+
Loading datasets...
|
| 1248 |
+
</div>
|
| 1249 |
+
</div>
|
| 1250 |
+
</div>
|
| 1251 |
+
</div>
|
| 1252 |
+
|
| 1253 |
+
<div class="card">
|
| 1254 |
+
<div class="card-header">
|
| 1255 |
+
<h2 class="card-title">
|
| 1256 |
+
<svg class="icon icon-lg">
|
| 1257 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 1258 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 1259 |
+
</svg>
|
| 1260 |
+
Search Registry
|
| 1261 |
+
</h2>
|
| 1262 |
+
</div>
|
| 1263 |
+
<div style="display: flex; gap: 12px; margin-bottom: 20px;">
|
| 1264 |
+
<input type="text" id="hfSearchQuery" placeholder="Search crypto, bitcoin, sentiment..." class="input" style="flex: 1;" value="crypto">
|
| 1265 |
+
<select id="hfSearchKind" class="input" style="width: auto; padding: 12px 16px;">
|
| 1266 |
+
<option value="models">Models</option>
|
| 1267 |
+
<option value="datasets">Datasets</option>
|
| 1268 |
+
</select>
|
| 1269 |
+
<button class="btn" onclick="searchHF()">
|
| 1270 |
+
<svg class="icon">
|
| 1271 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 1272 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 1273 |
+
</svg>
|
| 1274 |
+
Search
|
| 1275 |
+
</button>
|
| 1276 |
+
</div>
|
| 1277 |
+
<div id="hfSearchResults" style="max-height: 400px; overflow-y: auto; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
|
| 1278 |
+
<div style="text-align: center; color: var(--text-muted);">Enter a query and click search</div>
|
| 1279 |
+
</div>
|
| 1280 |
+
</div>
|
| 1281 |
+
|
| 1282 |
+
<div class="card">
|
| 1283 |
+
<div class="card-header">
|
| 1284 |
+
<h2 class="card-title">💭 Sentiment Analysis</h2>
|
| 1285 |
+
</div>
|
| 1286 |
+
<div style="margin-bottom: 16px;">
|
| 1287 |
+
<label style="display: block; font-weight: 700; margin-bottom: 8px; color: var(--text-primary);">Text Samples (one per line)</label>
|
| 1288 |
+
<textarea id="hfSentimentTexts" rows="6" class="input" placeholder="BTC strong breakout ETH looks weak Crypto market is bullish today">BTC strong breakout
|
| 1289 |
+
ETH looks weak
|
| 1290 |
+
Crypto market is bullish today
|
| 1291 |
+
Bears are taking control
|
| 1292 |
+
Neutral market conditions</textarea>
|
| 1293 |
+
</div>
|
| 1294 |
+
<button class="btn" onclick="runHFSentiment()">
|
| 1295 |
+
<svg class="icon">
|
| 1296 |
+
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
|
| 1297 |
+
</svg>
|
| 1298 |
+
Run Sentiment Analysis
|
| 1299 |
+
</button>
|
| 1300 |
+
<div id="hfSentimentVote" style="margin: 20px 0; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); text-align: center; font-size: 32px; font-weight: 900; border: 2px solid var(--border);">
|
| 1301 |
+
<span style="color: var(--text-muted);">—</span>
|
| 1302 |
+
</div>
|
| 1303 |
+
<div id="hfSentimentResults" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 400px; overflow-y: auto; border: 2px solid var(--border);">
|
| 1304 |
+
Results will appear here...
|
| 1305 |
+
</div>
|
| 1306 |
+
</div>
|
| 1307 |
+
</div>
|
| 1308 |
+
</div>
|
| 1309 |
+
|
| 1310 |
+
<script>
|
| 1311 |
+
// Configuration - آپدیت شده برای Hugging Face Spaces
|
| 1312 |
+
const config = {
|
| 1313 |
+
// استفاده از آدرس نسبی برای Hugging Face
|
| 1314 |
+
apiBaseUrl: '', // خالی بذارید تا از همون origin استفاده کنه
|
| 1315 |
+
wsUrl: (() => {
|
| 1316 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 1317 |
+
const host = window.location.host;
|
| 1318 |
+
return `${protocol}//${host}/ws`;
|
| 1319 |
+
})(),
|
| 1320 |
+
autoRefreshInterval: 30000,
|
| 1321 |
+
maxRetries: 3
|
| 1322 |
+
};
|
| 1323 |
+
|
| 1324 |
+
// Global state
|
| 1325 |
+
let state = {
|
| 1326 |
+
ws: null,
|
| 1327 |
+
wsConnected: false,
|
| 1328 |
+
autoRefreshEnabled: true,
|
| 1329 |
+
charts: {},
|
| 1330 |
+
currentTab: 'dashboard',
|
| 1331 |
+
providers: [],
|
| 1332 |
+
categories: [],
|
| 1333 |
+
rateLimits: [],
|
| 1334 |
+
logs: [],
|
| 1335 |
+
alerts: [],
|
| 1336 |
+
lastUpdate: null
|
| 1337 |
+
};
|
| 1338 |
+
|
| 1339 |
+
// Initialize on page load
|
| 1340 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1341 |
+
console.log('🚀 Initializing Crypto API Monitor...');
|
| 1342 |
+
console.log('📍 API Base URL:', config.apiBaseUrl);
|
| 1343 |
+
console.log('📡 WebSocket URL:', config.wsUrl);
|
| 1344 |
+
|
| 1345 |
+
discoverEndpoints(); // کشف endpoint های موجود
|
| 1346 |
+
initializeWebSocket();
|
| 1347 |
+
loadInitialData();
|
| 1348 |
+
startAutoRefresh();
|
| 1349 |
+
});
|
| 1350 |
+
|
| 1351 |
+
// تابع برای کشف endpoint های موجود
|
| 1352 |
+
async function discoverEndpoints() {
|
| 1353 |
+
console.log('🔍 Discovering available endpoints...');
|
| 1354 |
+
|
| 1355 |
+
const testEndpoints = [
|
| 1356 |
+
'/',
|
| 1357 |
+
'/health',
|
| 1358 |
+
'/api/health',
|
| 1359 |
+
'/info',
|
| 1360 |
+
'/api/info',
|
| 1361 |
+
'/providers',
|
| 1362 |
+
'/api/providers',
|
| 1363 |
+
'/status',
|
| 1364 |
+
'/api/status',
|
| 1365 |
+
'/api/crypto/market-overview',
|
| 1366 |
+
'/api/crypto/prices/top'
|
| 1367 |
+
];
|
| 1368 |
+
|
| 1369 |
+
const availableEndpoints = [];
|
| 1370 |
+
|
| 1371 |
+
for (const endpoint of testEndpoints) {
|
| 1372 |
+
try {
|
| 1373 |
+
const response = await fetch(endpoint);
|
| 1374 |
+
console.log(`${endpoint}: ${response.status}`);
|
| 1375 |
+
if (response.ok) {
|
| 1376 |
+
availableEndpoints.push(endpoint);
|
| 1377 |
+
console.log(`✅ Found: ${endpoint}`);
|
| 1378 |
+
}
|
| 1379 |
+
} catch (error) {
|
| 1380 |
+
console.log(`❌ Failed: ${endpoint}`);
|
| 1381 |
+
}
|
| 1382 |
+
}
|
| 1383 |
+
|
| 1384 |
+
console.log('📋 Available endpoints:', availableEndpoints);
|
| 1385 |
+
return availableEndpoints;
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
// WebSocket Connection
|
| 1389 |
+
function initializeWebSocket() {
|
| 1390 |
+
updateWSStatus('connecting');
|
| 1391 |
+
|
| 1392 |
+
const wsEndpoints = [
|
| 1393 |
+
'/ws/live',
|
| 1394 |
+
'/ws',
|
| 1395 |
+
'/live',
|
| 1396 |
+
'/api/ws'
|
| 1397 |
+
];
|
| 1398 |
+
|
| 1399 |
+
for (const endpoint of wsEndpoints) {
|
| 1400 |
+
try {
|
| 1401 |
+
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${endpoint}`;
|
| 1402 |
+
console.log(`🔄 Trying WebSocket: ${wsUrl}`);
|
| 1403 |
+
|
| 1404 |
+
state.ws = new WebSocket(wsUrl);
|
| 1405 |
+
setupWebSocketHandlers();
|
| 1406 |
+
break; // اگر موفق بود، بقیه رو امتحان نکن
|
| 1407 |
+
} catch (error) {
|
| 1408 |
+
console.log(`❌ WebSocket failed: ${endpoint}`);
|
| 1409 |
+
}
|
| 1410 |
+
}
|
| 1411 |
+
|
| 1412 |
+
if (!state.ws) {
|
| 1413 |
+
console.log('⚠️ No WebSocket endpoints available');
|
| 1414 |
+
updateWSStatus('disconnected');
|
| 1415 |
+
}
|
| 1416 |
+
}
|
| 1417 |
+
|
| 1418 |
+
function setupWebSocketHandlers() {
|
| 1419 |
+
state.ws.onopen = () => {
|
| 1420 |
+
console.log('✅ WebSocket connected');
|
| 1421 |
+
state.wsConnected = true;
|
| 1422 |
+
updateWSStatus('connected');
|
| 1423 |
+
showToast('Connected', 'Real-time data stream active', 'success');
|
| 1424 |
+
};
|
| 1425 |
+
|
| 1426 |
+
state.ws.onmessage = (event) => {
|
| 1427 |
+
try {
|
| 1428 |
+
const data = JSON.parse(event.data);
|
| 1429 |
+
handleWSMessage(data);
|
| 1430 |
+
} catch (error) {
|
| 1431 |
+
console.error('Error parsing WebSocket message:', error);
|
| 1432 |
+
}
|
| 1433 |
+
};
|
| 1434 |
+
|
| 1435 |
+
state.ws.onerror = (error) => {
|
| 1436 |
+
console.error('❌ WebSocket error:', error);
|
| 1437 |
+
updateWSStatus('disconnected');
|
| 1438 |
+
};
|
| 1439 |
+
|
| 1440 |
+
state.ws.onclose = () => {
|
| 1441 |
+
console.log('⚠️ WebSocket disconnected');
|
| 1442 |
+
state.wsConnected = false;
|
| 1443 |
+
updateWSStatus('disconnected');
|
| 1444 |
+
};
|
| 1445 |
+
}
|
| 1446 |
+
|
| 1447 |
+
function updateWSStatus(status) {
|
| 1448 |
+
const statusEl = document.getElementById('wsStatus');
|
| 1449 |
+
const textEl = document.getElementById('wsStatusText');
|
| 1450 |
+
|
| 1451 |
+
statusEl.classList.remove('connected', 'disconnected', 'connecting');
|
| 1452 |
+
statusEl.classList.add(status);
|
| 1453 |
+
|
| 1454 |
+
const statusText = {
|
| 1455 |
+
'connected': '✓ Connected',
|
| 1456 |
+
'disconnected': '✗ Disconnected',
|
| 1457 |
+
'connecting': '⟳ Connecting...'
|
| 1458 |
+
};
|
| 1459 |
+
|
| 1460 |
+
textEl.textContent = statusText[status] || 'Unknown';
|
| 1461 |
+
}
|
| 1462 |
+
|
| 1463 |
+
function handleWSMessage(data) {
|
| 1464 |
+
console.log('📨 WebSocket message:', data.type);
|
| 1465 |
+
|
| 1466 |
+
switch(data.type) {
|
| 1467 |
+
case 'status_update':
|
| 1468 |
+
updateKPIs(data.data);
|
| 1469 |
+
break;
|
| 1470 |
+
case 'provider_status_change':
|
| 1471 |
+
loadProviders();
|
| 1472 |
+
break;
|
| 1473 |
+
case 'new_alert':
|
| 1474 |
+
addAlert(data.data);
|
| 1475 |
+
break;
|
| 1476 |
+
default:
|
| 1477 |
+
console.log('Unknown message type:', data.type);
|
| 1478 |
+
}
|
| 1479 |
+
}
|
| 1480 |
+
|
| 1481 |
+
// API Calls - آپدیت شده با endpoint های جایگزین
|
| 1482 |
+
async function apiCall(endpoint, options = {}) {
|
| 1483 |
+
try {
|
| 1484 |
+
const url = `${config.apiBaseUrl}${endpoint}`;
|
| 1485 |
+
console.log('🌐 API Call:', url);
|
| 1486 |
+
|
| 1487 |
+
const response = await fetch(url, {
|
| 1488 |
+
...options,
|
| 1489 |
+
headers: {
|
| 1490 |
+
'Content-Type': 'application/json',
|
| 1491 |
+
...options.headers
|
| 1492 |
+
}
|
| 1493 |
+
});
|
| 1494 |
+
|
| 1495 |
+
if (!response.ok) {
|
| 1496 |
+
// اگر 404 باشه، endpoint های جایگزین رو چک کن
|
| 1497 |
+
if (response.status === 404) {
|
| 1498 |
+
console.log(`⚠️ Endpoint ${endpoint} not found, trying alternatives...`);
|
| 1499 |
+
return await tryAlternativeEndpoints(endpoint, options);
|
| 1500 |
+
}
|
| 1501 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 1502 |
+
}
|
| 1503 |
+
|
| 1504 |
+
const data = await response.json();
|
| 1505 |
+
console.log('✅ API Response:', endpoint, data);
|
| 1506 |
+
return data;
|
| 1507 |
+
} catch (error) {
|
| 1508 |
+
console.error(`❌ API call failed: ${endpoint}`, error);
|
| 1509 |
+
showToast('API Error', `Failed: ${endpoint}`, 'error');
|
| 1510 |
+
throw error;
|
| 1511 |
+
}
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
// تابع برای امتحان endpoint های جایگزین
|
| 1515 |
+
async function tryAlternativeEndpoints(originalEndpoint, options) {
|
| 1516 |
+
const alternatives = {
|
| 1517 |
+
'/api/providers': ['/providers', '/api/sources', '/status'],
|
| 1518 |
+
'/health': ['/api/health', '/status/health'],
|
| 1519 |
+
'/info': ['/api/info', '/system/info'],
|
| 1520 |
+
'/api/categories': ['/categories', '/api/groups'],
|
| 1521 |
+
'/api/rate-limits': ['/rate-limits', '/api/limits'],
|
| 1522 |
+
'/api/logs': ['/logs', '/api/events'],
|
| 1523 |
+
'/api/alerts': ['/alerts', '/api/notifications'],
|
| 1524 |
+
'/api/hf/health': ['/hf/health', '/api/huggingface/health'],
|
| 1525 |
+
'/api/hf/refresh': ['/hf/refresh', '/api/huggingface/refresh'],
|
| 1526 |
+
'/api/hf/registry': ['/hf/registry', '/api/huggingface/registry'],
|
| 1527 |
+
'/api/hf/search': ['/hf/search', '/api/huggingface/search'],
|
| 1528 |
+
'/api/hf/run-sentiment': ['/hf/sentiment', '/api/huggingface/sentiment']
|
| 1529 |
+
};
|
| 1530 |
+
|
| 1531 |
+
for (const altEndpoint of alternatives[originalEndpoint] || []) {
|
| 1532 |
+
try {
|
| 1533 |
+
const url = `${config.apiBaseUrl}${altEndpoint}`;
|
| 1534 |
+
console.log(`🔄 Trying alternative: ${altEndpoint}`);
|
| 1535 |
+
|
| 1536 |
+
const response = await fetch(url, options);
|
| 1537 |
+
if (response.ok) {
|
| 1538 |
+
const data = await response.json();
|
| 1539 |
+
console.log(`✅ Alternative endpoint worked: ${altEndpoint}`);
|
| 1540 |
+
return data;
|
| 1541 |
+
}
|
| 1542 |
+
} catch (error) {
|
| 1543 |
+
console.log(`❌ Alternative failed: ${altEndpoint}`);
|
| 1544 |
+
}
|
| 1545 |
+
}
|
| 1546 |
+
|
| 1547 |
+
throw new Error(`All endpoints failed for ${originalEndpoint}`);
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
async function loadInitialData() {
|
| 1551 |
+
showLoading();
|
| 1552 |
+
|
| 1553 |
+
try {
|
| 1554 |
+
console.log('📊 Loading initial data...');
|
| 1555 |
+
|
| 1556 |
+
await loadHealth();
|
| 1557 |
+
await loadProviders();
|
| 1558 |
+
await loadSystemInfo();
|
| 1559 |
+
|
| 1560 |
+
initializeCharts();
|
| 1561 |
+
|
| 1562 |
+
state.lastUpdate = new Date();
|
| 1563 |
+
updateLastUpdateDisplay();
|
| 1564 |
+
|
| 1565 |
+
console.log('✅ Initial data loaded successfully');
|
| 1566 |
+
showToast('Success', 'Dashboard loaded successfully', 'success');
|
| 1567 |
+
} catch (error) {
|
| 1568 |
+
console.error('❌ Error loading initial data:', error);
|
| 1569 |
+
showToast('Error', 'Failed to load initial data', 'error');
|
| 1570 |
+
} finally {
|
| 1571 |
+
hideLoading();
|
| 1572 |
+
}
|
| 1573 |
+
}
|
| 1574 |
+
|
| 1575 |
+
async function loadHealth() {
|
| 1576 |
+
try {
|
| 1577 |
+
const data = await apiCall('/health');
|
| 1578 |
+
updateKPIs(data.components || data);
|
| 1579 |
+
|
| 1580 |
+
const statusBadge = document.getElementById('systemStatus');
|
| 1581 |
+
const statusText = document.getElementById('systemStatusText');
|
| 1582 |
+
|
| 1583 |
+
if (data.status === 'healthy') {
|
| 1584 |
+
statusBadge.style.background = 'linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.08))';
|
| 1585 |
+
statusBadge.style.borderColor = 'rgba(16, 185, 129, 0.4)';
|
| 1586 |
+
statusBadge.style.color = 'var(--success)';
|
| 1587 |
+
statusText.textContent = '✓ System Healthy';
|
| 1588 |
+
} else if (data.status === 'degraded') {
|
| 1589 |
+
statusBadge.style.background = 'linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.08))';
|
| 1590 |
+
statusBadge.style.borderColor = 'rgba(245, 158, 11, 0.4)';
|
| 1591 |
+
statusBadge.style.color = 'var(--warning)';
|
| 1592 |
+
statusText.textContent = '⚠ System Degraded';
|
| 1593 |
+
} else {
|
| 1594 |
+
statusBadge.style.background = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.08))';
|
| 1595 |
+
statusBadge.style.borderColor = 'rgba(239, 68, 68, 0.4)';
|
| 1596 |
+
statusBadge.style.color = 'var(--danger)';
|
| 1597 |
+
statusText.textContent = '✗ System Critical';
|
| 1598 |
+
}
|
| 1599 |
+
} catch (error) {
|
| 1600 |
+
console.error('Error loading health:', error);
|
| 1601 |
+
}
|
| 1602 |
+
}
|
| 1603 |
+
|
| 1604 |
+
async function loadSystemInfo() {
|
| 1605 |
+
try {
|
| 1606 |
+
const data = await apiCall('/info');
|
| 1607 |
+
console.log('📋 System Info:', data);
|
| 1608 |
+
} catch (error) {
|
| 1609 |
+
console.error('Error loading system info:', error);
|
| 1610 |
+
}
|
| 1611 |
+
}
|
| 1612 |
+
|
| 1613 |
+
async function loadProviders() {
|
| 1614 |
+
try {
|
| 1615 |
+
const data = await apiCall('/api/providers');
|
| 1616 |
+
state.providers = data;
|
| 1617 |
+
renderProvidersTable(data);
|
| 1618 |
+
renderProvidersDetail(data);
|
| 1619 |
+
updateStatusChart(data);
|
| 1620 |
+
} catch (error) {
|
| 1621 |
+
console.error('Error loading providers:', error);
|
| 1622 |
+
document.getElementById('providersTableBody').innerHTML = `
|
| 1623 |
+
<tr>
|
| 1624 |
+
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
| 1625 |
+
Failed to load providers. Please check if the API endpoint is available.
|
| 1626 |
+
</td>
|
| 1627 |
+
</tr>
|
| 1628 |
+
`;
|
| 1629 |
+
}
|
| 1630 |
+
}
|
| 1631 |
+
|
| 1632 |
+
async function loadCategories() {
|
| 1633 |
+
try {
|
| 1634 |
+
showLoading();
|
| 1635 |
+
const data = await apiCall('/api/categories');
|
| 1636 |
+
state.categories = data;
|
| 1637 |
+
renderCategoriesTable(data);
|
| 1638 |
+
showToast('Success', 'Categories loaded successfully', 'success');
|
| 1639 |
+
} catch (error) {
|
| 1640 |
+
console.error('Error loading categories:', error);
|
| 1641 |
+
showToast('Error', 'Failed to load categories', 'error');
|
| 1642 |
+
} finally {
|
| 1643 |
+
hideLoading();
|
| 1644 |
+
}
|
| 1645 |
+
}
|
| 1646 |
+
|
| 1647 |
+
async function loadRateLimits() {
|
| 1648 |
+
try {
|
| 1649 |
+
showLoading();
|
| 1650 |
+
const data = await apiCall('/api/rate-limits');
|
| 1651 |
+
state.rateLimits = data;
|
| 1652 |
+
renderRateLimitCards(data);
|
| 1653 |
+
showToast('Success', 'Rate limits loaded successfully', 'success');
|
| 1654 |
+
} catch (error) {
|
| 1655 |
+
console.error('Error loading rate limits:', error);
|
| 1656 |
+
showToast('Error', 'Failed to load rate limits', 'error');
|
| 1657 |
+
} finally {
|
| 1658 |
+
hideLoading();
|
| 1659 |
+
}
|
| 1660 |
+
}
|
| 1661 |
+
|
| 1662 |
+
async function loadLogs() {
|
| 1663 |
+
try {
|
| 1664 |
+
showLoading();
|
| 1665 |
+
const logType = document.getElementById('logType').value;
|
| 1666 |
+
const data = await apiCall(`/api/logs?type=${logType}`);
|
| 1667 |
+
state.logs = data;
|
| 1668 |
+
renderLogsTable(data);
|
| 1669 |
+
showToast('Success', 'Logs loaded successfully', 'success');
|
| 1670 |
+
} catch (error) {
|
| 1671 |
+
console.error('Error loading logs:', error);
|
| 1672 |
+
showToast('Error', 'Failed to load logs', 'error');
|
| 1673 |
+
} finally {
|
| 1674 |
+
hideLoading();
|
| 1675 |
+
}
|
| 1676 |
+
}
|
| 1677 |
+
|
| 1678 |
+
async function loadAlerts() {
|
| 1679 |
+
try {
|
| 1680 |
+
showLoading();
|
| 1681 |
+
const data = await apiCall('/api/alerts');
|
| 1682 |
+
state.alerts = data;
|
| 1683 |
+
renderAlertsList(data);
|
| 1684 |
+
showToast('Success', 'Alerts loaded successfully', 'success');
|
| 1685 |
+
} catch (error) {
|
| 1686 |
+
console.error('Error loading alerts:', error);
|
| 1687 |
+
showToast('Error', 'Failed to load alerts', 'error');
|
| 1688 |
+
} finally {
|
| 1689 |
+
hideLoading();
|
| 1690 |
+
}
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
// HuggingFace APIs
|
| 1694 |
+
async function loadHFHealth() {
|
| 1695 |
+
try {
|
| 1696 |
+
const data = await apiCall('/api/hf/health');
|
| 1697 |
+
document.getElementById('hfHealthDisplay').textContent = JSON.stringify(data, null, 2);
|
| 1698 |
+
} catch (error) {
|
| 1699 |
+
console.error('Error loading HF health:', error);
|
| 1700 |
+
document.getElementById('hfHealthDisplay').textContent = 'Error loading health status';
|
| 1701 |
+
}
|
| 1702 |
+
}
|
| 1703 |
+
|
| 1704 |
+
async function refreshHFRegistry() {
|
| 1705 |
+
try {
|
| 1706 |
+
showLoading();
|
| 1707 |
+
const data = await apiCall('/api/hf/refresh', { method: 'POST' });
|
| 1708 |
+
showToast('Success', 'HF Registry refreshed', 'success');
|
| 1709 |
+
loadHFModels();
|
| 1710 |
+
loadHFDatasets();
|
| 1711 |
+
} catch (error) {
|
| 1712 |
+
console.error('Error refreshing HF registry:', error);
|
| 1713 |
+
showToast('Error', 'Failed to refresh registry', 'error');
|
| 1714 |
+
} finally {
|
| 1715 |
+
hideLoading();
|
| 1716 |
+
}
|
| 1717 |
+
}
|
| 1718 |
+
|
| 1719 |
+
async function loadHFModels() {
|
| 1720 |
+
try {
|
| 1721 |
+
const data = await apiCall('/api/hf/registry?type=models');
|
| 1722 |
+
document.getElementById('hfModelsCount').textContent = data.length || 0;
|
| 1723 |
+
document.getElementById('hfModelsList').innerHTML = data.map(item => `
|
| 1724 |
+
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
|
| 1725 |
+
<div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
|
| 1726 |
+
<div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
|
| 1727 |
+
</div>
|
| 1728 |
+
`).join('');
|
| 1729 |
+
} catch (error) {
|
| 1730 |
+
console.error('Error loading HF models:', error);
|
| 1731 |
+
document.getElementById('hfModelsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading models</div>';
|
| 1732 |
+
}
|
| 1733 |
+
}
|
| 1734 |
+
|
| 1735 |
+
async function loadHFDatasets() {
|
| 1736 |
+
try {
|
| 1737 |
+
const data = await apiCall('/api/hf/registry?type=datasets');
|
| 1738 |
+
document.getElementById('hfDatasetsCount').textContent = data.length || 0;
|
| 1739 |
+
document.getElementById('hfDatasetsList').innerHTML = data.map(item => `
|
| 1740 |
+
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
|
| 1741 |
+
<div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
|
| 1742 |
+
<div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
|
| 1743 |
+
</div>
|
| 1744 |
+
`).join('');
|
| 1745 |
+
} catch (error) {
|
| 1746 |
+
console.error('Error loading HF datasets:', error);
|
| 1747 |
+
document.getElementById('hfDatasetsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading datasets</div>';
|
| 1748 |
+
}
|
| 1749 |
+
}
|
| 1750 |
+
|
| 1751 |
+
async function searchHF() {
|
| 1752 |
+
try {
|
| 1753 |
+
showLoading();
|
| 1754 |
+
const query = document.getElementById('hfSearchQuery').value;
|
| 1755 |
+
const kind = document.getElementById('hfSearchKind').value;
|
| 1756 |
+
const data = await apiCall(`/api/hf/search?q=${encodeURIComponent(query)}&kind=${kind}`);
|
| 1757 |
+
|
| 1758 |
+
document.getElementById('hfSearchResults').innerHTML = data.map(item => `
|
| 1759 |
+
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
|
| 1760 |
+
<div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
|
| 1761 |
+
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">${item.description || 'No description'}</div>
|
| 1762 |
+
<div style="font-size: 11px; color: var(--accent-primary);">Downloads: ${item.downloads || 0} • Likes: ${item.likes || 0}</div>
|
| 1763 |
+
</div>
|
| 1764 |
+
`).join('');
|
| 1765 |
+
|
| 1766 |
+
showToast('Success', `Found ${data.length} results`, 'success');
|
| 1767 |
+
} catch (error) {
|
| 1768 |
+
console.error('Error searching HF:', error);
|
| 1769 |
+
showToast('Error', 'Search failed', 'error');
|
| 1770 |
+
} finally {
|
| 1771 |
+
hideLoading();
|
| 1772 |
+
}
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
async function runHFSentiment() {
|
| 1776 |
+
try {
|
| 1777 |
+
showLoading();
|
| 1778 |
+
const texts = document.getElementById('hfSentimentTexts').value.split('\n').filter(t => t.trim());
|
| 1779 |
+
|
| 1780 |
+
const data = await apiCall('/api/hf/run-sentiment', {
|
| 1781 |
+
method: 'POST',
|
| 1782 |
+
body: JSON.stringify({ texts: texts })
|
| 1783 |
+
});
|
| 1784 |
+
|
| 1785 |
+
document.getElementById('hfSentimentResults').textContent = JSON.stringify(data, null, 2);
|
| 1786 |
+
|
| 1787 |
+
// Calculate overall sentiment vote
|
| 1788 |
+
const sentiments = data.results || [];
|
| 1789 |
+
const positive = sentiments.filter(s => s.sentiment === 'positive').length;
|
| 1790 |
+
const negative = sentiments.filter(s => s.sentiment === 'negative').length;
|
| 1791 |
+
const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;
|
| 1792 |
+
|
| 1793 |
+
let overall = 'NEUTRAL';
|
| 1794 |
+
let color = 'var(--info)';
|
| 1795 |
+
|
| 1796 |
+
if (positive > negative && positive > neutral) {
|
| 1797 |
+
overall = 'BULLISH 📈';
|
| 1798 |
+
color = 'var(--success)';
|
| 1799 |
+
} else if (negative > positive && negative > neutral) {
|
| 1800 |
+
overall = 'BEARISH 📉';
|
| 1801 |
+
color = 'var(--danger)';
|
| 1802 |
+
}
|
| 1803 |
+
|
| 1804 |
+
document.getElementById('hfSentimentVote').innerHTML = `
|
| 1805 |
+
<span style="color: ${color};">${overall}</span>
|
| 1806 |
+
<div style="font-size: 14px; margin-top: 8px; color: var(--text-muted);">
|
| 1807 |
+
Positive: ${positive} • Negative: ${negative} • Neutral: ${neutral}
|
| 1808 |
+
</div>
|
| 1809 |
+
`;
|
| 1810 |
+
|
| 1811 |
+
showToast('Success', 'Sentiment analysis completed', 'success');
|
| 1812 |
+
} catch (error) {
|
| 1813 |
+
console.error('Error running sentiment analysis:', error);
|
| 1814 |
+
showToast('Error', 'Sentiment analysis failed', 'error');
|
| 1815 |
+
} finally {
|
| 1816 |
+
hideLoading();
|
| 1817 |
+
}
|
| 1818 |
+
}
|
| 1819 |
+
|
| 1820 |
+
// Rendering Functions
|
| 1821 |
+
function renderProvidersTable(providers) {
|
| 1822 |
+
const tbody = document.getElementById('providersTableBody');
|
| 1823 |
+
|
| 1824 |
+
if (!providers || providers.length === 0) {
|
| 1825 |
+
tbody.innerHTML = `
|
| 1826 |
+
<tr>
|
| 1827 |
+
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
| 1828 |
+
No providers found
|
| 1829 |
+
</td>
|
| 1830 |
+
</tr>
|
| 1831 |
+
`;
|
| 1832 |
+
return;
|
| 1833 |
+
}
|
| 1834 |
+
|
| 1835 |
+
tbody.innerHTML = providers.map(provider => `
|
| 1836 |
+
<tr>
|
| 1837 |
+
<td>
|
| 1838 |
+
<strong>${provider.name || 'Unknown'}</strong>
|
| 1839 |
+
<div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
|
| 1840 |
+
</td>
|
| 1841 |
+
<td>${provider.category || 'General'}</td>
|
| 1842 |
+
<td>
|
| 1843 |
+
<span class="badge ${getStatusBadgeClass(provider.status)}">
|
| 1844 |
+
${provider.status || 'unknown'}
|
| 1845 |
+
</span>
|
| 1846 |
+
</td>
|
| 1847 |
+
<td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
|
| 1848 |
+
<td>
|
| 1849 |
+
<div style="font-size: 12px; font-weight: 700;">${formatTimestamp(provider.last_checked)}</div>
|
| 1850 |
+
<div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(provider.last_checked)}</div>
|
| 1851 |
+
</td>
|
| 1852 |
+
</tr>
|
| 1853 |
+
`).join('');
|
| 1854 |
+
}
|
| 1855 |
+
|
| 1856 |
+
function renderProvidersDetail(providers) {
|
| 1857 |
+
const container = document.getElementById('providersDetail');
|
| 1858 |
+
|
| 1859 |
+
if (!providers || providers.length === 0) {
|
| 1860 |
+
container.innerHTML = `
|
| 1861 |
+
<div style="text-align: center; color: var(--text-muted); padding: 40px;">
|
| 1862 |
+
No providers data available
|
| 1863 |
+
</div>
|
| 1864 |
+
`;
|
| 1865 |
+
return;
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
container.innerHTML = `
|
| 1869 |
+
<div class="table-container">
|
| 1870 |
+
<table class="table">
|
| 1871 |
+
<thead>
|
| 1872 |
+
<tr>
|
| 1873 |
+
<th>Provider</th>
|
| 1874 |
+
<th>Status</th>
|
| 1875 |
+
<th>Response Time</th>
|
| 1876 |
+
<th>Success Rate</th>
|
| 1877 |
+
<th>Last Success</th>
|
| 1878 |
+
<th>Errors (24h)</th>
|
| 1879 |
+
</tr>
|
| 1880 |
+
</thead>
|
| 1881 |
+
<tbody>
|
| 1882 |
+
${providers.map(provider => `
|
| 1883 |
+
<tr>
|
| 1884 |
+
<td>
|
| 1885 |
+
<strong>${provider.name || 'Unknown'}</strong>
|
| 1886 |
+
<div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
|
| 1887 |
+
</td>
|
| 1888 |
+
<td>
|
| 1889 |
+
<span class="badge ${getStatusBadgeClass(provider.status)}">
|
| 1890 |
+
${provider.status || 'unknown'}
|
| 1891 |
+
</span>
|
| 1892 |
+
</td>
|
| 1893 |
+
<td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
|
| 1894 |
+
<td>
|
| 1895 |
+
<div class="progress">
|
| 1896 |
+
<div class="progress-bar ${getHealthClass(provider.success_rate || 0)}"
|
| 1897 |
+
style="width: ${provider.success_rate || 0}%"></div>
|
| 1898 |
+
</div>
|
| 1899 |
+
<small>${Math.round(provider.success_rate || 0)}%</small>
|
| 1900 |
+
</td>
|
| 1901 |
+
<td>${formatTimestamp(provider.last_success)}</td>
|
| 1902 |
+
<td>${provider.error_count_24h || 0}</td>
|
| 1903 |
+
</tr>
|
| 1904 |
+
`).join('')}
|
| 1905 |
+
</tbody>
|
| 1906 |
+
</table>
|
| 1907 |
+
</div>
|
| 1908 |
+
`;
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
function renderCategoriesTable(categories) {
|
| 1912 |
+
const tbody = document.getElementById('categoriesTableBody');
|
| 1913 |
+
|
| 1914 |
+
if (!categories || categories.length === 0) {
|
| 1915 |
+
tbody.innerHTML = `
|
| 1916 |
+
<tr>
|
| 1917 |
+
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
| 1918 |
+
No categories found
|
| 1919 |
+
</td>
|
| 1920 |
+
</tr>
|
| 1921 |
+
`;
|
| 1922 |
+
return;
|
| 1923 |
+
}
|
| 1924 |
+
|
| 1925 |
+
tbody.innerHTML = categories.map(category => `
|
| 1926 |
+
<tr>
|
| 1927 |
+
<td>
|
| 1928 |
+
<strong>${category.name || 'Unnamed'}</strong>
|
| 1929 |
+
</td>
|
| 1930 |
+
<td>${category.total_sources || 0}</td>
|
| 1931 |
+
<td>${category.online || 0}</td>
|
| 1932 |
+
<td>
|
| 1933 |
+
<div class="progress">
|
| 1934 |
+
<div class="progress-bar ${getHealthClass(category.health_percentage || 0)}"
|
| 1935 |
+
style="width: ${category.health_percentage || 0}%"></div>
|
| 1936 |
+
</div>
|
| 1937 |
+
<small>${Math.round(category.health_percentage || 0)}%</small>
|
| 1938 |
+
</td>
|
| 1939 |
+
<td>${category.avg_response || '--'}ms</td>
|
| 1940 |
+
<td>${formatTimestamp(category.last_updated)}</td>
|
| 1941 |
+
<td>
|
| 1942 |
+
<span class="badge ${getStatusBadgeClass(category.status)}">
|
| 1943 |
+
${category.status || 'unknown'}
|
| 1944 |
+
</span>
|
| 1945 |
+
</td>
|
| 1946 |
+
</tr>
|
| 1947 |
+
`).join('');
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
+
function renderRateLimitCards(rateLimits) {
|
| 1951 |
+
const container = document.getElementById('rateLimitCards');
|
| 1952 |
+
|
| 1953 |
+
if (!rateLimits || rateLimits.length === 0) {
|
| 1954 |
+
container.innerHTML = `
|
| 1955 |
+
<div class="card" style="grid-column: 1 / -1; text-align: center; padding: 40px;">
|
| 1956 |
+
<div style="color: var(--text-muted); font-size: 16px;">
|
| 1957 |
+
No rate limit data available
|
| 1958 |
+
</div>
|
| 1959 |
+
</div>
|
| 1960 |
+
`;
|
| 1961 |
+
return;
|
| 1962 |
+
}
|
| 1963 |
+
|
| 1964 |
+
container.innerHTML = rateLimits.map(limit => `
|
| 1965 |
+
<div class="card">
|
| 1966 |
+
<div class="card-header">
|
| 1967 |
+
<h3 class="card-title" style="font-size: 16px;">
|
| 1968 |
+
${limit.provider || 'Unknown Provider'}
|
| 1969 |
+
</h3>
|
| 1970 |
+
<span class="badge ${getRateLimitStatusClass(limit)}">
|
| 1971 |
+
${getRateLimitStatus(limit)}
|
| 1972 |
+
</span>
|
| 1973 |
+
</div>
|
| 1974 |
+
|
| 1975 |
+
<div style="margin-bottom: 16px;">
|
| 1976 |
+
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
|
| 1977 |
+
<span style="font-size: 12px; color: var(--text-muted);">Usage</span>
|
| 1978 |
+
<span style="font-size: 12px; font-weight: 700;">
|
| 1979 |
+
${limit.used || 0}/${limit.limit || 0}
|
| 1980 |
+
</span>
|
| 1981 |
+
</div>
|
| 1982 |
+
<div class="progress">
|
| 1983 |
+
<div class="progress-bar ${getRateLimitProgressClass(limit)}"
|
| 1984 |
+
style="width: ${calculateUsagePercentage(limit)}%"></div>
|
| 1985 |
+
</div>
|
| 1986 |
+
</div>
|
| 1987 |
+
|
| 1988 |
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 12px;">
|
| 1989 |
+
<div>
|
| 1990 |
+
<div style="color: var(--text-muted);">Reset In</div>
|
| 1991 |
+
<div style="font-weight: 700;">${formatResetTime(limit.reset_time)}</div>
|
| 1992 |
+
</div>
|
| 1993 |
+
<div>
|
| 1994 |
+
<div style="color: var(--text-muted);">Window</div>
|
| 1995 |
+
<div style="font-weight: 700;">${limit.window || 'N/A'}</div>
|
| 1996 |
+
</div>
|
| 1997 |
+
</div>
|
| 1998 |
+
|
| 1999 |
+
${limit.endpoint ? `
|
| 2000 |
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
|
| 2001 |
+
<div style="font-size: 11px; color: var(--text-muted);">Endpoint</div>
|
| 2002 |
+
<div style="font-size: 12px; font-family: monospace;">${limit.endpoint}</div>
|
| 2003 |
+
</div>
|
| 2004 |
+
` : ''}
|
| 2005 |
+
</div>
|
| 2006 |
+
`).join('');
|
| 2007 |
+
}
|
| 2008 |
+
|
| 2009 |
+
function renderLogsTable(logs) {
|
| 2010 |
+
const tbody = document.getElementById('logsTableBody');
|
| 2011 |
+
|
| 2012 |
+
if (!logs || logs.length === 0) {
|
| 2013 |
+
tbody.innerHTML = `
|
| 2014 |
+
<tr>
|
| 2015 |
+
<td colspan="6" style="text-align: center; color: var(--text-muted); padding: 40px;">
|
| 2016 |
+
No logs found
|
| 2017 |
+
</td>
|
| 2018 |
+
</tr>
|
| 2019 |
+
`;
|
| 2020 |
+
return;
|
| 2021 |
+
}
|
| 2022 |
+
|
| 2023 |
+
tbody.innerHTML = logs.map(log => `
|
| 2024 |
+
<tr>
|
| 2025 |
+
<td>
|
| 2026 |
+
<div style="font-size: 12px; font-weight: 700;">${formatTimestamp(log.timestamp)}</div>
|
| 2027 |
+
<div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(log.timestamp)}</div>
|
| 2028 |
+
</td>
|
| 2029 |
+
<td>
|
| 2030 |
+
<strong>${log.provider || 'System'}</strong>
|
| 2031 |
+
</td>
|
| 2032 |
+
<td>
|
| 2033 |
+
<span class="badge ${getLogTypeClass(log.type)}">
|
| 2034 |
+
${log.type || 'unknown'}
|
| 2035 |
+
</span>
|
| 2036 |
+
</td>
|
| 2037 |
+
<td>
|
| 2038 |
+
<span class="badge ${getStatusBadgeClass(log.status)}">
|
| 2039 |
+
${log.status || 'unknown'}
|
| 2040 |
+
</span>
|
| 2041 |
+
</td>
|
| 2042 |
+
<td>${log.response_time ? log.response_time + 'ms' : '--'}</td>
|
| 2043 |
+
<td>
|
| 2044 |
+
<div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
| 2045 |
+
${log.message || 'No message'}
|
| 2046 |
+
</div>
|
| 2047 |
+
</td>
|
| 2048 |
+
</tr>
|
| 2049 |
+
`).join('');
|
| 2050 |
+
}
|
| 2051 |
+
|
| 2052 |
+
function renderAlertsList(alerts) {
|
| 2053 |
+
const container = document.getElementById('alertsList');
|
| 2054 |
+
|
| 2055 |
+
if (!alerts || alerts.length === 0) {
|
| 2056 |
+
container.innerHTML = `
|
| 2057 |
+
<div class="alert alert-success">
|
| 2058 |
+
<div class="alert-content">
|
| 2059 |
+
<div class="alert-title">No Active Alerts</div>
|
| 2060 |
+
<div class="alert-message">All systems are operating normally</div>
|
| 2061 |
+
</div>
|
| 2062 |
+
</div>
|
| 2063 |
+
`;
|
| 2064 |
+
return;
|
| 2065 |
+
}
|
| 2066 |
+
|
| 2067 |
+
container.innerHTML = alerts.map(alert => `
|
| 2068 |
+
<div class="alert ${getAlertClass(alert.severity)}">
|
| 2069 |
+
<div class="alert-content">
|
| 2070 |
+
<div class="alert-title">
|
| 2071 |
+
${alert.title || 'Alert'}
|
| 2072 |
+
<span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
|
| 2073 |
+
${formatTimeAgo(alert.timestamp)}
|
| 2074 |
+
</span>
|
| 2075 |
+
</div>
|
| 2076 |
+
<div class="alert-message">
|
| 2077 |
+
${alert.message || 'No message provided'}
|
| 2078 |
+
</div>
|
| 2079 |
+
${alert.provider ? `
|
| 2080 |
+
<div style="margin-top: 8px; font-size: 12px;">
|
| 2081 |
+
<strong>Provider:</strong> ${alert.provider}
|
| 2082 |
+
</div>
|
| 2083 |
+
` : ''}
|
| 2084 |
+
</div>
|
| 2085 |
+
</div>
|
| 2086 |
+
`).join('');
|
| 2087 |
+
}
|
| 2088 |
+
|
| 2089 |
+
// Helper functions
|
| 2090 |
+
function getHealthClass(percentage) {
|
| 2091 |
+
if (percentage >= 80) return 'success';
|
| 2092 |
+
if (percentage >= 60) return 'warning';
|
| 2093 |
+
return 'danger';
|
| 2094 |
+
}
|
| 2095 |
+
|
| 2096 |
+
function getStatusBadgeClass(status) {
|
| 2097 |
+
switch (status?.toLowerCase()) {
|
| 2098 |
+
case 'healthy': case 'online': case 'success': return 'badge-success';
|
| 2099 |
+
case 'degraded': case 'warning': return 'badge-warning';
|
| 2100 |
+
case 'offline': case 'error': case 'critical': return 'badge-danger';
|
| 2101 |
+
default: return 'badge-info';
|
| 2102 |
+
}
|
| 2103 |
+
}
|
| 2104 |
+
|
| 2105 |
+
function getLogTypeClass(type) {
|
| 2106 |
+
switch (type?.toLowerCase()) {
|
| 2107 |
+
case 'error': return 'badge-danger';
|
| 2108 |
+
case 'warning': return 'badge-warning';
|
| 2109 |
+
case 'info': case 'connection': return 'badge-info';
|
| 2110 |
+
case 'success': return 'badge-success';
|
| 2111 |
+
default: return 'badge-info';
|
| 2112 |
+
}
|
| 2113 |
+
}
|
| 2114 |
+
|
| 2115 |
+
function getAlertClass(severity) {
|
| 2116 |
+
switch (severity?.toLowerCase()) {
|
| 2117 |
+
case 'critical': case 'error': return 'alert-danger';
|
| 2118 |
+
case 'warning': return 'alert-warning';
|
| 2119 |
+
case 'info': return 'alert-info';
|
| 2120 |
+
case 'success': return 'alert-success';
|
| 2121 |
+
default: return 'alert-info';
|
| 2122 |
+
}
|
| 2123 |
+
}
|
| 2124 |
+
|
| 2125 |
+
function getRateLimitStatusClass(limit) {
|
| 2126 |
+
const usage = calculateUsagePercentage(limit);
|
| 2127 |
+
if (usage >= 90) return 'badge-danger';
|
| 2128 |
+
if (usage >= 75) return 'badge-warning';
|
| 2129 |
+
return 'badge-success';
|
| 2130 |
+
}
|
| 2131 |
+
|
| 2132 |
+
function getRateLimitStatus(limit) {
|
| 2133 |
+
const usage = calculateUsagePercentage(limit);
|
| 2134 |
+
if (usage >= 90) return 'Critical';
|
| 2135 |
+
if (usage >= 75) return 'Warning';
|
| 2136 |
+
return 'Normal';
|
| 2137 |
+
}
|
| 2138 |
+
|
| 2139 |
+
function getRateLimitProgressClass(limit) {
|
| 2140 |
+
const usage = calculateUsagePercentage(limit);
|
| 2141 |
+
if (usage >= 90) return 'danger';
|
| 2142 |
+
if (usage >= 75) return 'warning';
|
| 2143 |
+
return 'success';
|
| 2144 |
+
}
|
| 2145 |
+
|
| 2146 |
+
function calculateUsagePercentage(limit) {
|
| 2147 |
+
if (!limit.limit || limit.limit === 0) return 0;
|
| 2148 |
+
return Math.min(100, ((limit.used || 0) / limit.limit) * 100);
|
| 2149 |
+
}
|
| 2150 |
+
|
| 2151 |
+
function formatResetTime(resetTime) {
|
| 2152 |
+
if (!resetTime) return 'N/A';
|
| 2153 |
+
// Simple formatting - you can enhance this with proper time parsing
|
| 2154 |
+
return typeof resetTime === 'string' ? resetTime : 'Soon';
|
| 2155 |
+
}
|
| 2156 |
+
|
| 2157 |
+
function formatTimestamp(timestamp) {
|
| 2158 |
+
if (!timestamp) return '--';
|
| 2159 |
+
try {
|
| 2160 |
+
return new Date(timestamp).toLocaleString();
|
| 2161 |
+
} catch {
|
| 2162 |
+
return 'Invalid Date';
|
| 2163 |
+
}
|
| 2164 |
+
}
|
| 2165 |
+
|
| 2166 |
+
function formatTimeAgo(timestamp) {
|
| 2167 |
+
if (!timestamp) return '';
|
| 2168 |
+
try {
|
| 2169 |
+
const now = new Date();
|
| 2170 |
+
const time = new Date(timestamp);
|
| 2171 |
+
const diff = now - time;
|
| 2172 |
+
|
| 2173 |
+
const minutes = Math.floor(diff / 60000);
|
| 2174 |
+
const hours = Math.floor(diff / 3600000);
|
| 2175 |
+
const days = Math.floor(diff / 86400000);
|
| 2176 |
+
|
| 2177 |
+
if (days > 0) return `${days}d ago`;
|
| 2178 |
+
if (hours > 0) return `${hours}h ago`;
|
| 2179 |
+
if (minutes > 0) return `${minutes}m ago`;
|
| 2180 |
+
return 'Just now';
|
| 2181 |
+
} catch {
|
| 2182 |
+
return 'Unknown';
|
| 2183 |
+
}
|
| 2184 |
+
}
|
| 2185 |
+
|
| 2186 |
+
// KPI Updates
|
| 2187 |
+
function updateKPIs(data) {
|
| 2188 |
+
if (!data) return;
|
| 2189 |
+
|
| 2190 |
+
// Update Total APIs
|
| 2191 |
+
const totalAPIs = data.length || 0;
|
| 2192 |
+
document.getElementById('kpiTotalAPIs').textContent = totalAPIs;
|
| 2193 |
+
document.getElementById('kpiTotalTrend').textContent = `${totalAPIs} active`;
|
| 2194 |
+
|
| 2195 |
+
// Update Online count
|
| 2196 |
+
const onlineCount = data.filter(p => p.status === 'online' || p.status === 'healthy').length;
|
| 2197 |
+
document.getElementById('kpiOnline').textContent = onlineCount;
|
| 2198 |
+
document.getElementById('kpiOnlineTrend').textContent = `${Math.round((onlineCount / totalAPIs) * 100)}% uptime`;
|
| 2199 |
+
|
| 2200 |
+
// Update Average Response
|
| 2201 |
+
const validResponses = data.filter(p => p.response_time).map(p => p.response_time);
|
| 2202 |
+
const avgResponse = validResponses.length > 0 ?
|
| 2203 |
+
Math.round(validResponses.reduce((a, b) => a + b, 0) / validResponses.length) : 0;
|
| 2204 |
+
|
| 2205 |
+
document.getElementById('kpiAvgResponse').textContent = avgResponse + 'ms';
|
| 2206 |
+
|
| 2207 |
+
const responseTrend = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
|
| 2208 |
+
document.getElementById('kpiResponseTrend').textContent = responseTrend;
|
| 2209 |
+
|
| 2210 |
+
const trendElement = document.querySelector('#kpiAvgResponse').nextElementSibling;
|
| 2211 |
+
trendElement.className = `kpi-trend ${
|
| 2212 |
+
avgResponse < 500 ? 'trend-up' : avgResponse < 1000 ? 'trend-neutral' : 'trend-down'
|
| 2213 |
+
}`;
|
| 2214 |
+
}
|
| 2215 |
+
|
| 2216 |
+
function updateLastUpdateDisplay() {
|
| 2217 |
+
if (state.lastUpdate) {
|
| 2218 |
+
document.getElementById('kpiLastUpdate').textContent =
|
| 2219 |
+
state.lastUpdate.toLocaleTimeString() + '\n' + state.lastUpdate.toLocaleDateString();
|
| 2220 |
+
}
|
| 2221 |
+
}
|
| 2222 |
+
|
| 2223 |
+
// Chart Functions
|
| 2224 |
+
function initializeCharts() {
|
| 2225 |
+
// Health Chart
|
| 2226 |
+
const healthCtx = document.getElementById('healthChart').getContext('2d');
|
| 2227 |
+
state.charts.health = new Chart(healthCtx, {
|
| 2228 |
+
type: 'line',
|
| 2229 |
+
data: {
|
| 2230 |
+
labels: [],
|
| 2231 |
+
datasets: [{
|
| 2232 |
+
label: 'System Health %',
|
| 2233 |
+
data: [],
|
| 2234 |
+
borderColor: '#8b5cf6',
|
| 2235 |
+
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
| 2236 |
+
borderWidth: 3,
|
| 2237 |
+
fill: true,
|
| 2238 |
+
tension: 0.4
|
| 2239 |
+
}]
|
| 2240 |
+
},
|
| 2241 |
+
options: {
|
| 2242 |
+
responsive: true,
|
| 2243 |
+
maintainAspectRatio: false,
|
| 2244 |
+
plugins: {
|
| 2245 |
+
legend: {
|
| 2246 |
+
display: false
|
| 2247 |
+
}
|
| 2248 |
+
},
|
| 2249 |
+
scales: {
|
| 2250 |
+
y: {
|
| 2251 |
+
beginAtZero: true,
|
| 2252 |
+
max: 100,
|
| 2253 |
+
grid: {
|
| 2254 |
+
color: 'rgba(139, 92, 246, 0.1)'
|
| 2255 |
+
}
|
| 2256 |
+
},
|
| 2257 |
+
x: {
|
| 2258 |
+
grid: {
|
| 2259 |
+
color: 'rgba(139, 92, 246, 0.1)'
|
| 2260 |
+
}
|
| 2261 |
+
}
|
| 2262 |
+
}
|
| 2263 |
+
}
|
| 2264 |
+
});
|
| 2265 |
+
|
| 2266 |
+
// Status Chart
|
| 2267 |
+
const statusCtx = document.getElementById('statusChart').getContext('2d');
|
| 2268 |
+
state.charts.status = new Chart(statusCtx, {
|
| 2269 |
+
type: 'doughnut',
|
| 2270 |
+
data: {
|
| 2271 |
+
labels: ['Online', 'Degraded', 'Offline'],
|
| 2272 |
+
datasets: [{
|
| 2273 |
+
data: [0, 0, 0],
|
| 2274 |
+
backgroundColor: [
|
| 2275 |
+
'#10b981',
|
| 2276 |
+
'#f59e0b',
|
| 2277 |
+
'#ef4444'
|
| 2278 |
+
],
|
| 2279 |
+
borderWidth: 0
|
| 2280 |
+
}]
|
| 2281 |
+
},
|
| 2282 |
+
options: {
|
| 2283 |
+
responsive: true,
|
| 2284 |
+
maintainAspectRatio: false,
|
| 2285 |
+
cutout: '70%',
|
| 2286 |
+
plugins: {
|
| 2287 |
+
legend: {
|
| 2288 |
+
position: 'bottom'
|
| 2289 |
+
}
|
| 2290 |
+
}
|
| 2291 |
+
}
|
| 2292 |
+
});
|
| 2293 |
+
}
|
| 2294 |
+
|
| 2295 |
+
function updateStatusChart(providers) {
|
| 2296 |
+
if (!state.charts.status || !providers) return;
|
| 2297 |
+
|
| 2298 |
+
const online = providers.filter(p => p.status === 'online' || p.status === 'healthy').length;
|
| 2299 |
+
const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'warning').length;
|
| 2300 |
+
const offline = providers.filter(p => p.status === 'offline' || p.status === 'error').length;
|
| 2301 |
+
|
| 2302 |
+
state.charts.status.data.datasets[0].data = [online, degraded, offline];
|
| 2303 |
+
state.charts.status.update();
|
| 2304 |
+
}
|
| 2305 |
+
|
| 2306 |
+
// Tab Management
|
| 2307 |
+
function switchTab(event, tabName) {
|
| 2308 |
+
// Remove active class from all tabs
|
| 2309 |
+
document.querySelectorAll('.tab').forEach(tab => {
|
| 2310 |
+
tab.classList.remove('active');
|
| 2311 |
+
});
|
| 2312 |
+
|
| 2313 |
+
// Remove active class from all contents
|
| 2314 |
+
document.querySelectorAll('.tab-content').forEach(content => {
|
| 2315 |
+
content.classList.remove('active');
|
| 2316 |
+
});
|
| 2317 |
+
|
| 2318 |
+
// Add active class to clicked tab
|
| 2319 |
+
event.currentTarget.classList.add('active');
|
| 2320 |
+
|
| 2321 |
+
// Show corresponding content
|
| 2322 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 2323 |
+
|
| 2324 |
+
// Load data for the tab
|
| 2325 |
+
switch(tabName) {
|
| 2326 |
+
case 'dashboard':
|
| 2327 |
+
loadProviders();
|
| 2328 |
+
break;
|
| 2329 |
+
case 'providers':
|
| 2330 |
+
loadProviders();
|
| 2331 |
+
break;
|
| 2332 |
+
case 'categories':
|
| 2333 |
+
loadCategories();
|
| 2334 |
+
break;
|
| 2335 |
+
case 'ratelimits':
|
| 2336 |
+
loadRateLimits();
|
| 2337 |
+
break;
|
| 2338 |
+
case 'logs':
|
| 2339 |
+
loadLogs();
|
| 2340 |
+
break;
|
| 2341 |
+
case 'alerts':
|
| 2342 |
+
loadAlerts();
|
| 2343 |
+
break;
|
| 2344 |
+
case 'huggingface':
|
| 2345 |
+
loadHFHealth();
|
| 2346 |
+
loadHFModels();
|
| 2347 |
+
loadHFDatasets();
|
| 2348 |
+
break;
|
| 2349 |
+
}
|
| 2350 |
+
|
| 2351 |
+
state.currentTab = tabName;
|
| 2352 |
+
}
|
| 2353 |
+
|
| 2354 |
+
// Utility Functions
|
| 2355 |
+
function showLoading() {
|
| 2356 |
+
document.getElementById('loadingOverlay').classList.add('active');
|
| 2357 |
+
}
|
| 2358 |
+
|
| 2359 |
+
function hideLoading() {
|
| 2360 |
+
document.getElementById('loadingOverlay').classList.remove('active');
|
| 2361 |
+
}
|
| 2362 |
+
|
| 2363 |
+
function showToast(title, message, type = 'info') {
|
| 2364 |
+
const container = document.getElementById('toastContainer');
|
| 2365 |
+
const toast = document.createElement('div');
|
| 2366 |
+
toast.className = `toast ${type}`;
|
| 2367 |
+
toast.innerHTML = `
|
| 2368 |
+
<div class="toast-content">
|
| 2369 |
+
<div class="toast-title">${title}</div>
|
| 2370 |
+
<div class="toast-message">${message}</div>
|
| 2371 |
+
</div>
|
| 2372 |
+
<button onclick="this.parentElement.remove()" style="background: none; border: none; cursor: pointer; color: inherit;">
|
| 2373 |
+
<svg class="icon" style="width: 16px; height: 16px;">
|
| 2374 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 2375 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 2376 |
+
</svg>
|
| 2377 |
+
</button>
|
| 2378 |
+
`;
|
| 2379 |
+
|
| 2380 |
+
container.appendChild(toast);
|
| 2381 |
+
|
| 2382 |
+
// Auto remove after 5 seconds
|
| 2383 |
+
setTimeout(() => {
|
| 2384 |
+
if (toast.parentElement) {
|
| 2385 |
+
toast.remove();
|
| 2386 |
+
}
|
| 2387 |
+
}, 5000);
|
| 2388 |
+
}
|
| 2389 |
+
|
| 2390 |
+
function addAlert(alert) {
|
| 2391 |
+
const container = document.getElementById('alertsContainer');
|
| 2392 |
+
const alertEl = document.createElement('div');
|
| 2393 |
+
alertEl.className = `alert ${getAlertClass(alert.severity)}`;
|
| 2394 |
+
alertEl.innerHTML = `
|
| 2395 |
+
<div class="alert-content">
|
| 2396 |
+
<div class="alert-title">
|
| 2397 |
+
${alert.title}
|
| 2398 |
+
<span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
|
| 2399 |
+
${formatTimeAgo(alert.timestamp)}
|
| 2400 |
+
</span>
|
| 2401 |
+
</div>
|
| 2402 |
+
<div class="alert-message">${alert.message}</div>
|
| 2403 |
+
</div>
|
| 2404 |
+
`;
|
| 2405 |
+
|
| 2406 |
+
container.appendChild(alertEl);
|
| 2407 |
+
|
| 2408 |
+
// Auto remove after 10 seconds
|
| 2409 |
+
setTimeout(() => {
|
| 2410 |
+
if (alertEl.parentElement) {
|
| 2411 |
+
alertEl.remove();
|
| 2412 |
+
}
|
| 2413 |
+
}, 10000);
|
| 2414 |
+
}
|
| 2415 |
+
|
| 2416 |
+
function refreshAll() {
|
| 2417 |
+
console.log('🔄 Refreshing all data...');
|
| 2418 |
+
loadInitialData();
|
| 2419 |
+
|
| 2420 |
+
// Refresh current tab data
|
| 2421 |
+
switch(state.currentTab) {
|
| 2422 |
+
case 'categories':
|
| 2423 |
+
loadCategories();
|
| 2424 |
+
break;
|
| 2425 |
+
case 'ratelimits':
|
| 2426 |
+
loadRateLimits();
|
| 2427 |
+
break;
|
| 2428 |
+
case 'logs':
|
| 2429 |
+
loadLogs();
|
| 2430 |
+
break;
|
| 2431 |
+
case 'alerts':
|
| 2432 |
+
loadAlerts();
|
| 2433 |
+
break;
|
| 2434 |
+
case 'huggingface':
|
| 2435 |
+
loadHFHealth();
|
| 2436 |
+
loadHFModels();
|
| 2437 |
+
loadHFDatasets();
|
| 2438 |
+
break;
|
| 2439 |
+
}
|
| 2440 |
+
}
|
| 2441 |
+
|
| 2442 |
+
function startAutoRefresh() {
|
| 2443 |
+
setInterval(() => {
|
| 2444 |
+
if (state.autoRefreshEnabled && state.wsConnected) {
|
| 2445 |
+
console.log('🔄 Auto-refreshing data...');
|
| 2446 |
+
refreshAll();
|
| 2447 |
+
}
|
| 2448 |
+
}, config.autoRefreshInterval);
|
| 2449 |
+
}
|
| 2450 |
+
</script>
|
| 2451 |
+
</body>
|
| 2452 |
+
</html>
|
archive_html/index_enhanced.html
ADDED
|
@@ -0,0 +1,2132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>🚀 Crypto API Monitor - Professional Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
:root {
|
| 11 |
+
--bg-primary: #0f0f23;
|
| 12 |
+
--bg-secondary: #1a1a2e;
|
| 13 |
+
--bg-card: #16213e;
|
| 14 |
+
--bg-hover: #1f2b4d;
|
| 15 |
+
--text-primary: #ffffff;
|
| 16 |
+
--text-secondary: #b8c1ec;
|
| 17 |
+
--text-muted: #8892b0;
|
| 18 |
+
|
| 19 |
+
--accent-blue: #00d4ff;
|
| 20 |
+
--accent-purple: #a855f7;
|
| 21 |
+
--accent-pink: #ec4899;
|
| 22 |
+
--accent-green: #10b981;
|
| 23 |
+
--accent-yellow: #fbbf24;
|
| 24 |
+
--accent-red: #ef4444;
|
| 25 |
+
--accent-orange: #f97316;
|
| 26 |
+
--accent-cyan: #06b6d4;
|
| 27 |
+
|
| 28 |
+
--success: #10b981;
|
| 29 |
+
--success-glow: rgba(16, 185, 129, 0.3);
|
| 30 |
+
--warning: #fbbf24;
|
| 31 |
+
--warning-glow: rgba(251, 191, 36, 0.3);
|
| 32 |
+
--danger: #ef4444;
|
| 33 |
+
--danger-glow: rgba(239, 68, 68, 0.3);
|
| 34 |
+
--info: #00d4ff;
|
| 35 |
+
--info-glow: rgba(0, 212, 255, 0.3);
|
| 36 |
+
|
| 37 |
+
--border: rgba(255, 255, 255, 0.1);
|
| 38 |
+
--shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
| 39 |
+
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.6);
|
| 40 |
+
--glow: 0 0 20px;
|
| 41 |
+
|
| 42 |
+
--radius: 16px;
|
| 43 |
+
--radius-lg: 24px;
|
| 44 |
+
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
* {
|
| 48 |
+
margin: 0;
|
| 49 |
+
padding: 0;
|
| 50 |
+
box-sizing: border-box;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
body {
|
| 54 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 55 |
+
background: var(--bg-primary);
|
| 56 |
+
color: var(--text-primary);
|
| 57 |
+
line-height: 1.6;
|
| 58 |
+
min-height: 100vh;
|
| 59 |
+
overflow-x: hidden;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/* Animated Background */
|
| 63 |
+
body::before {
|
| 64 |
+
content: '';
|
| 65 |
+
position: fixed;
|
| 66 |
+
top: 0;
|
| 67 |
+
left: 0;
|
| 68 |
+
right: 0;
|
| 69 |
+
bottom: 0;
|
| 70 |
+
background:
|
| 71 |
+
radial-gradient(circle at 20% 30%, rgba(168, 85, 247, 0.15) 0%, transparent 50%),
|
| 72 |
+
radial-gradient(circle at 80% 70%, rgba(0, 212, 255, 0.15) 0%, transparent 50%),
|
| 73 |
+
radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.1) 0%, transparent 70%);
|
| 74 |
+
pointer-events: none;
|
| 75 |
+
z-index: 0;
|
| 76 |
+
animation: backgroundPulse 20s ease-in-out infinite;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
@keyframes backgroundPulse {
|
| 80 |
+
0%, 100% { opacity: 0.5; }
|
| 81 |
+
50% { opacity: 0.8; }
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.container {
|
| 85 |
+
max-width: 1800px;
|
| 86 |
+
margin: 0 auto;
|
| 87 |
+
padding: 32px;
|
| 88 |
+
position: relative;
|
| 89 |
+
z-index: 1;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Header */
|
| 93 |
+
.header {
|
| 94 |
+
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
|
| 95 |
+
border: 2px solid var(--border);
|
| 96 |
+
border-radius: var(--radius-lg);
|
| 97 |
+
padding: 40px;
|
| 98 |
+
margin-bottom: 32px;
|
| 99 |
+
box-shadow: var(--shadow-lg);
|
| 100 |
+
position: relative;
|
| 101 |
+
overflow: hidden;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.header::before {
|
| 105 |
+
content: '';
|
| 106 |
+
position: absolute;
|
| 107 |
+
top: 0;
|
| 108 |
+
left: 0;
|
| 109 |
+
right: 0;
|
| 110 |
+
height: 4px;
|
| 111 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
|
| 112 |
+
animation: gradientSlide 3s linear infinite;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
@keyframes gradientSlide {
|
| 116 |
+
0% { transform: translateX(-100%); }
|
| 117 |
+
100% { transform: translateX(100%); }
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.header-top {
|
| 121 |
+
display: flex;
|
| 122 |
+
align-items: center;
|
| 123 |
+
justify-content: space-between;
|
| 124 |
+
flex-wrap: wrap;
|
| 125 |
+
gap: 24px;
|
| 126 |
+
margin-bottom: 32px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.logo {
|
| 130 |
+
display: flex;
|
| 131 |
+
align-items: center;
|
| 132 |
+
gap: 20px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.logo-icon {
|
| 136 |
+
width: 80px;
|
| 137 |
+
height: 80px;
|
| 138 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
|
| 139 |
+
border-radius: 24px;
|
| 140 |
+
display: flex;
|
| 141 |
+
align-items: center;
|
| 142 |
+
justify-content: center;
|
| 143 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 144 |
+
position: relative;
|
| 145 |
+
overflow: hidden;
|
| 146 |
+
animation: logoFloat 3s ease-in-out infinite;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
@keyframes logoFloat {
|
| 150 |
+
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
| 151 |
+
50% { transform: translateY(-10px) rotate(5deg); }
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.logo-icon::after {
|
| 155 |
+
content: '';
|
| 156 |
+
position: absolute;
|
| 157 |
+
top: -50%;
|
| 158 |
+
left: -50%;
|
| 159 |
+
right: -50%;
|
| 160 |
+
bottom: -50%;
|
| 161 |
+
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
| 162 |
+
animation: shimmer 2s infinite;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
@keyframes shimmer {
|
| 166 |
+
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
|
| 167 |
+
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.logo-text h1 {
|
| 171 |
+
font-size: 36px;
|
| 172 |
+
font-weight: 900;
|
| 173 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
|
| 174 |
+
-webkit-background-clip: text;
|
| 175 |
+
-webkit-text-fill-color: transparent;
|
| 176 |
+
background-clip: text;
|
| 177 |
+
margin-bottom: 8px;
|
| 178 |
+
letter-spacing: -1px;
|
| 179 |
+
text-shadow: 0 0 30px var(--info-glow);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.logo-text p {
|
| 183 |
+
font-size: 14px;
|
| 184 |
+
color: var(--text-secondary);
|
| 185 |
+
font-weight: 600;
|
| 186 |
+
letter-spacing: 0.5px;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.header-actions {
|
| 190 |
+
display: flex;
|
| 191 |
+
gap: 16px;
|
| 192 |
+
align-items: center;
|
| 193 |
+
flex-wrap: wrap;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.status-badge {
|
| 197 |
+
display: flex;
|
| 198 |
+
align-items: center;
|
| 199 |
+
gap: 12px;
|
| 200 |
+
padding: 14px 24px;
|
| 201 |
+
border-radius: 999px;
|
| 202 |
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(16, 185, 129, 0.1) 100%);
|
| 203 |
+
border: 2px solid var(--success);
|
| 204 |
+
font-size: 14px;
|
| 205 |
+
font-weight: 700;
|
| 206 |
+
color: var(--success);
|
| 207 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 208 |
+
text-transform: uppercase;
|
| 209 |
+
letter-spacing: 1px;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.status-dot {
|
| 213 |
+
width: 12px;
|
| 214 |
+
height: 12px;
|
| 215 |
+
border-radius: 50%;
|
| 216 |
+
background: var(--success);
|
| 217 |
+
animation: pulse-glow 2s infinite;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
@keyframes pulse-glow {
|
| 221 |
+
0%, 100% {
|
| 222 |
+
box-shadow: 0 0 0 0 var(--success-glow),
|
| 223 |
+
0 0 15px var(--success);
|
| 224 |
+
}
|
| 225 |
+
50% {
|
| 226 |
+
box-shadow: 0 0 0 10px transparent,
|
| 227 |
+
0 0 25px var(--success);
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.connection-status {
|
| 232 |
+
display: flex;
|
| 233 |
+
align-items: center;
|
| 234 |
+
gap: 10px;
|
| 235 |
+
padding: 12px 20px;
|
| 236 |
+
border-radius: 999px;
|
| 237 |
+
background: var(--bg-secondary);
|
| 238 |
+
border: 2px solid var(--border);
|
| 239 |
+
font-size: 13px;
|
| 240 |
+
font-weight: 700;
|
| 241 |
+
transition: var(--transition);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.connection-status.connected {
|
| 245 |
+
border-color: var(--success);
|
| 246 |
+
color: var(--success);
|
| 247 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 248 |
+
}
|
| 249 |
+
.connection-status.disconnected {
|
| 250 |
+
border-color: var(--danger);
|
| 251 |
+
color: var(--danger);
|
| 252 |
+
box-shadow: var(--glow) var(--danger-glow);
|
| 253 |
+
}
|
| 254 |
+
.connection-status.connecting {
|
| 255 |
+
border-color: var(--warning);
|
| 256 |
+
color: var(--warning);
|
| 257 |
+
box-shadow: var(--glow) var(--warning-glow);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.btn {
|
| 261 |
+
padding: 14px 28px;
|
| 262 |
+
border-radius: 14px;
|
| 263 |
+
border: none;
|
| 264 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
| 265 |
+
color: white;
|
| 266 |
+
font-family: inherit;
|
| 267 |
+
font-size: 14px;
|
| 268 |
+
font-weight: 700;
|
| 269 |
+
cursor: pointer;
|
| 270 |
+
transition: var(--transition);
|
| 271 |
+
display: inline-flex;
|
| 272 |
+
align-items: center;
|
| 273 |
+
gap: 10px;
|
| 274 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 275 |
+
text-transform: uppercase;
|
| 276 |
+
letter-spacing: 0.5px;
|
| 277 |
+
position: relative;
|
| 278 |
+
overflow: hidden;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.btn::before {
|
| 282 |
+
content: '';
|
| 283 |
+
position: absolute;
|
| 284 |
+
top: 0;
|
| 285 |
+
left: -100%;
|
| 286 |
+
width: 100%;
|
| 287 |
+
height: 100%;
|
| 288 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 289 |
+
transition: left 0.5s;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.btn:hover {
|
| 293 |
+
transform: translateY(-3px) scale(1.05);
|
| 294 |
+
box-shadow: 0 15px 40px var(--info-glow);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.btn:hover::before {
|
| 298 |
+
left: 100%;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.btn:active {
|
| 302 |
+
transform: translateY(-1px) scale(1.02);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
/* KPI Cards */
|
| 306 |
+
.kpi-grid {
|
| 307 |
+
display: grid;
|
| 308 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 309 |
+
gap: 24px;
|
| 310 |
+
margin-bottom: 32px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.kpi-card {
|
| 314 |
+
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
|
| 315 |
+
border: 2px solid var(--border);
|
| 316 |
+
border-radius: var(--radius-lg);
|
| 317 |
+
padding: 32px;
|
| 318 |
+
transition: var(--transition);
|
| 319 |
+
box-shadow: var(--shadow);
|
| 320 |
+
position: relative;
|
| 321 |
+
overflow: hidden;
|
| 322 |
+
cursor: pointer;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.kpi-card::before {
|
| 326 |
+
content: '';
|
| 327 |
+
position: absolute;
|
| 328 |
+
top: 0;
|
| 329 |
+
left: 0;
|
| 330 |
+
right: 0;
|
| 331 |
+
height: 4px;
|
| 332 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
|
| 333 |
+
transform: scaleX(0);
|
| 334 |
+
transform-origin: left;
|
| 335 |
+
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.kpi-card:hover {
|
| 339 |
+
transform: translateY(-12px) scale(1.03);
|
| 340 |
+
box-shadow: 0 20px 60px rgba(0, 212, 255, 0.3);
|
| 341 |
+
border-color: var(--accent-blue);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.kpi-card:hover::before {
|
| 345 |
+
transform: scaleX(1);
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.kpi-header {
|
| 349 |
+
display: flex;
|
| 350 |
+
align-items: center;
|
| 351 |
+
justify-content: space-between;
|
| 352 |
+
margin-bottom: 24px;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.kpi-label {
|
| 356 |
+
font-size: 12px;
|
| 357 |
+
color: var(--text-muted);
|
| 358 |
+
font-weight: 800;
|
| 359 |
+
text-transform: uppercase;
|
| 360 |
+
letter-spacing: 1.5px;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.kpi-icon-wrapper {
|
| 364 |
+
width: 72px;
|
| 365 |
+
height: 72px;
|
| 366 |
+
border-radius: 20px;
|
| 367 |
+
display: flex;
|
| 368 |
+
align-items: center;
|
| 369 |
+
justify-content: center;
|
| 370 |
+
transition: var(--transition);
|
| 371 |
+
box-shadow: var(--shadow);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.kpi-card:hover .kpi-icon-wrapper {
|
| 375 |
+
transform: rotate(-10deg) scale(1.2);
|
| 376 |
+
box-shadow: 0 15px 40px rgba(0, 212, 255, 0.4);
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.kpi-value {
|
| 380 |
+
font-size: 56px;
|
| 381 |
+
font-weight: 900;
|
| 382 |
+
margin-bottom: 20px;
|
| 383 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 50%, var(--accent-pink) 100%);
|
| 384 |
+
-webkit-background-clip: text;
|
| 385 |
+
-webkit-text-fill-color: transparent;
|
| 386 |
+
background-clip: text;
|
| 387 |
+
line-height: 1;
|
| 388 |
+
animation: countUp 0.6s ease-out;
|
| 389 |
+
letter-spacing: -3px;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
@keyframes countUp {
|
| 393 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 394 |
+
to { opacity: 1; transform: translateY(0); }
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
.kpi-trend {
|
| 398 |
+
display: flex;
|
| 399 |
+
align-items: center;
|
| 400 |
+
gap: 10px;
|
| 401 |
+
font-size: 13px;
|
| 402 |
+
font-weight: 700;
|
| 403 |
+
padding: 10px 18px;
|
| 404 |
+
border-radius: 12px;
|
| 405 |
+
width: fit-content;
|
| 406 |
+
text-transform: uppercase;
|
| 407 |
+
letter-spacing: 0.5px;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.trend-up {
|
| 411 |
+
color: var(--success);
|
| 412 |
+
background: rgba(16, 185, 129, 0.15);
|
| 413 |
+
border: 2px solid var(--success);
|
| 414 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.trend-down {
|
| 418 |
+
color: var(--danger);
|
| 419 |
+
background: rgba(239, 68, 68, 0.15);
|
| 420 |
+
border: 2px solid var(--danger);
|
| 421 |
+
box-shadow: var(--glow) var(--danger-glow);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.trend-neutral {
|
| 425 |
+
color: var(--info);
|
| 426 |
+
background: rgba(0, 212, 255, 0.15);
|
| 427 |
+
border: 2px solid var(--info);
|
| 428 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* Tabs */
|
| 432 |
+
.tabs {
|
| 433 |
+
display: flex;
|
| 434 |
+
gap: 8px;
|
| 435 |
+
margin-bottom: 32px;
|
| 436 |
+
overflow-x: auto;
|
| 437 |
+
padding: 12px;
|
| 438 |
+
background: var(--bg-card);
|
| 439 |
+
border-radius: var(--radius-lg);
|
| 440 |
+
border: 2px solid var(--border);
|
| 441 |
+
box-shadow: var(--shadow);
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.tab {
|
| 445 |
+
padding: 14px 24px;
|
| 446 |
+
border-radius: 14px;
|
| 447 |
+
background: transparent;
|
| 448 |
+
border: 2px solid transparent;
|
| 449 |
+
color: var(--text-secondary);
|
| 450 |
+
cursor: pointer;
|
| 451 |
+
transition: all 0.25s;
|
| 452 |
+
white-space: nowrap;
|
| 453 |
+
font-weight: 700;
|
| 454 |
+
font-size: 14px;
|
| 455 |
+
display: flex;
|
| 456 |
+
align-items: center;
|
| 457 |
+
gap: 10px;
|
| 458 |
+
position: relative;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.tab:hover:not(.active) {
|
| 462 |
+
background: var(--bg-hover);
|
| 463 |
+
color: var(--text-primary);
|
| 464 |
+
border-color: var(--border);
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.tab.active {
|
| 468 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
| 469 |
+
color: white;
|
| 470 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 471 |
+
transform: scale(1.05);
|
| 472 |
+
border-color: var(--accent-blue);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.tab .icon {
|
| 476 |
+
width: 18px;
|
| 477 |
+
height: 18px;
|
| 478 |
+
stroke: currentColor;
|
| 479 |
+
stroke-width: 2.5;
|
| 480 |
+
stroke-linecap: round;
|
| 481 |
+
stroke-linejoin: round;
|
| 482 |
+
fill: none;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
/* Tab Content */
|
| 486 |
+
.tab-content {
|
| 487 |
+
display: none;
|
| 488 |
+
animation: fadeIn 0.5s ease;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.tab-content.active {
|
| 492 |
+
display: block;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
@keyframes fadeIn {
|
| 496 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 497 |
+
to { opacity: 1; transform: translateY(0); }
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/* Card */
|
| 501 |
+
.card {
|
| 502 |
+
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-secondary) 100%);
|
| 503 |
+
border: 2px solid var(--border);
|
| 504 |
+
border-radius: var(--radius-lg);
|
| 505 |
+
padding: 32px;
|
| 506 |
+
margin-bottom: 32px;
|
| 507 |
+
box-shadow: var(--shadow);
|
| 508 |
+
transition: var(--transition);
|
| 509 |
+
position: relative;
|
| 510 |
+
overflow: hidden;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
.card::before {
|
| 514 |
+
content: '';
|
| 515 |
+
position: absolute;
|
| 516 |
+
top: 0;
|
| 517 |
+
left: 0;
|
| 518 |
+
width: 100%;
|
| 519 |
+
height: 100%;
|
| 520 |
+
background: radial-gradient(circle at top right, rgba(0, 212, 255, 0.05) 0%, transparent 70%);
|
| 521 |
+
pointer-events: none;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.card:hover {
|
| 525 |
+
box-shadow: var(--shadow-lg);
|
| 526 |
+
border-color: var(--accent-blue);
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
.card-header {
|
| 530 |
+
display: flex;
|
| 531 |
+
align-items: center;
|
| 532 |
+
justify-content: space-between;
|
| 533 |
+
margin-bottom: 28px;
|
| 534 |
+
padding-bottom: 20px;
|
| 535 |
+
border-bottom: 2px solid var(--border);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.card-title {
|
| 539 |
+
font-size: 22px;
|
| 540 |
+
font-weight: 800;
|
| 541 |
+
display: flex;
|
| 542 |
+
align-items: center;
|
| 543 |
+
gap: 14px;
|
| 544 |
+
color: var(--text-primary);
|
| 545 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
| 546 |
+
-webkit-background-clip: text;
|
| 547 |
+
-webkit-text-fill-color: transparent;
|
| 548 |
+
background-clip: text;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
/* Table */
|
| 552 |
+
.table-container {
|
| 553 |
+
overflow-x: auto;
|
| 554 |
+
border-radius: var(--radius);
|
| 555 |
+
border: 2px solid var(--border);
|
| 556 |
+
background: var(--bg-secondary);
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
.table {
|
| 560 |
+
width: 100%;
|
| 561 |
+
border-collapse: collapse;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.table thead {
|
| 565 |
+
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
| 566 |
+
position: sticky;
|
| 567 |
+
top: 0;
|
| 568 |
+
z-index: 10;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
.table thead th {
|
| 572 |
+
color: white;
|
| 573 |
+
font-weight: 700;
|
| 574 |
+
font-size: 13px;
|
| 575 |
+
text-align: left;
|
| 576 |
+
padding: 18px 20px;
|
| 577 |
+
text-transform: uppercase;
|
| 578 |
+
letter-spacing: 1px;
|
| 579 |
+
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.table tbody tr {
|
| 583 |
+
transition: var(--transition);
|
| 584 |
+
border-bottom: 1px solid var(--border);
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.table tbody tr:hover {
|
| 588 |
+
background: var(--bg-hover);
|
| 589 |
+
transform: scale(1.01);
|
| 590 |
+
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.1);
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.table tbody td {
|
| 594 |
+
padding: 18px 20px;
|
| 595 |
+
font-size: 14px;
|
| 596 |
+
color: var(--text-secondary);
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.table tbody td strong {
|
| 600 |
+
color: var(--text-primary);
|
| 601 |
+
font-weight: 700;
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
/* Badge */
|
| 605 |
+
.badge {
|
| 606 |
+
display: inline-flex;
|
| 607 |
+
align-items: center;
|
| 608 |
+
gap: 8px;
|
| 609 |
+
padding: 8px 14px;
|
| 610 |
+
border-radius: 999px;
|
| 611 |
+
font-size: 12px;
|
| 612 |
+
font-weight: 700;
|
| 613 |
+
white-space: nowrap;
|
| 614 |
+
text-transform: uppercase;
|
| 615 |
+
letter-spacing: 0.5px;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.badge-success {
|
| 619 |
+
background: rgba(16, 185, 129, 0.2);
|
| 620 |
+
color: var(--success);
|
| 621 |
+
border: 2px solid var(--success);
|
| 622 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.badge-warning {
|
| 626 |
+
background: rgba(251, 191, 36, 0.2);
|
| 627 |
+
color: var(--warning);
|
| 628 |
+
border: 2px solid var(--warning);
|
| 629 |
+
box-shadow: var(--glow) var(--warning-glow);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.badge-danger {
|
| 633 |
+
background: rgba(239, 68, 68, 0.2);
|
| 634 |
+
color: var(--danger);
|
| 635 |
+
border: 2px solid var(--danger);
|
| 636 |
+
box-shadow: var(--glow) var(--danger-glow);
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
.badge-info {
|
| 640 |
+
background: rgba(0, 212, 255, 0.2);
|
| 641 |
+
color: var(--info);
|
| 642 |
+
border: 2px solid var(--info);
|
| 643 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 644 |
+
}
|
| 645 |
+
|
| 646 |
+
/* Progress Bar */
|
| 647 |
+
.progress {
|
| 648 |
+
height: 14px;
|
| 649 |
+
background: var(--bg-secondary);
|
| 650 |
+
border-radius: 999px;
|
| 651 |
+
overflow: hidden;
|
| 652 |
+
margin: 10px 0;
|
| 653 |
+
border: 2px solid var(--border);
|
| 654 |
+
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
.progress-bar {
|
| 658 |
+
height: 100%;
|
| 659 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
|
| 660 |
+
border-radius: 999px;
|
| 661 |
+
transition: width 0.5s ease;
|
| 662 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 663 |
+
position: relative;
|
| 664 |
+
overflow: hidden;
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
.progress-bar::after {
|
| 668 |
+
content: '';
|
| 669 |
+
position: absolute;
|
| 670 |
+
top: 0;
|
| 671 |
+
left: 0;
|
| 672 |
+
right: 0;
|
| 673 |
+
bottom: 0;
|
| 674 |
+
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
| 675 |
+
animation: progressShine 2s infinite;
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
@keyframes progressShine {
|
| 679 |
+
0% { transform: translateX(-100%); }
|
| 680 |
+
100% { transform: translateX(100%); }
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.progress-bar.success {
|
| 684 |
+
background: linear-gradient(90deg, var(--success), #34d399);
|
| 685 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.progress-bar.warning {
|
| 689 |
+
background: linear-gradient(90deg, var(--warning), #fbbf24);
|
| 690 |
+
box-shadow: var(--glow) var(--warning-glow);
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
.progress-bar.danger {
|
| 694 |
+
background: linear-gradient(90deg, var(--danger), #f87171);
|
| 695 |
+
box-shadow: var(--glow) var(--danger-glow);
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
/* Loading */
|
| 699 |
+
.loading-overlay {
|
| 700 |
+
position: fixed;
|
| 701 |
+
top: 0;
|
| 702 |
+
left: 0;
|
| 703 |
+
right: 0;
|
| 704 |
+
bottom: 0;
|
| 705 |
+
background: rgba(15, 15, 35, 0.95);
|
| 706 |
+
backdrop-filter: blur(10px);
|
| 707 |
+
display: none;
|
| 708 |
+
align-items: center;
|
| 709 |
+
justify-content: center;
|
| 710 |
+
z-index: 9999;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.loading-overlay.active {
|
| 714 |
+
display: flex;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.spinner {
|
| 718 |
+
width: 80px;
|
| 719 |
+
height: 80px;
|
| 720 |
+
border: 6px solid var(--border);
|
| 721 |
+
border-top-color: var(--accent-blue);
|
| 722 |
+
border-right-color: var(--accent-purple);
|
| 723 |
+
border-radius: 50%;
|
| 724 |
+
animation: spin 1s linear infinite;
|
| 725 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
@keyframes spin {
|
| 729 |
+
to { transform: rotate(360deg); }
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
/* Toast */
|
| 733 |
+
.toast-container {
|
| 734 |
+
position: fixed;
|
| 735 |
+
bottom: 32px;
|
| 736 |
+
right: 32px;
|
| 737 |
+
z-index: 10000;
|
| 738 |
+
display: flex;
|
| 739 |
+
flex-direction: column;
|
| 740 |
+
gap: 16px;
|
| 741 |
+
max-width: 400px;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
.toast {
|
| 745 |
+
padding: 20px 24px;
|
| 746 |
+
border-radius: var(--radius);
|
| 747 |
+
background: var(--bg-card);
|
| 748 |
+
border: 2px solid var(--border);
|
| 749 |
+
box-shadow: var(--shadow-lg);
|
| 750 |
+
display: flex;
|
| 751 |
+
align-items: center;
|
| 752 |
+
gap: 14px;
|
| 753 |
+
animation: slideInRight 0.4s ease;
|
| 754 |
+
min-width: 320px;
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
@keyframes slideInRight {
|
| 758 |
+
from { transform: translateX(500px); opacity: 0; }
|
| 759 |
+
to { transform: translateX(0); opacity: 1; }
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
.toast.success {
|
| 763 |
+
border-color: var(--success);
|
| 764 |
+
background: rgba(16, 185, 129, 0.1);
|
| 765 |
+
box-shadow: var(--glow) var(--success-glow);
|
| 766 |
+
}
|
| 767 |
+
.toast.error {
|
| 768 |
+
border-color: var(--danger);
|
| 769 |
+
background: rgba(239, 68, 68, 0.1);
|
| 770 |
+
box-shadow: var(--glow) var(--danger-glow);
|
| 771 |
+
}
|
| 772 |
+
.toast.warning {
|
| 773 |
+
border-color: var(--warning);
|
| 774 |
+
background: rgba(251, 191, 36, 0.1);
|
| 775 |
+
box-shadow: var(--glow) var(--warning-glow);
|
| 776 |
+
}
|
| 777 |
+
.toast.info {
|
| 778 |
+
border-color: var(--info);
|
| 779 |
+
background: rgba(0, 212, 255, 0.1);
|
| 780 |
+
box-shadow: var(--glow) var(--info-glow);
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
/* Grid */
|
| 784 |
+
.grid { display: grid; gap: 24px; }
|
| 785 |
+
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); }
|
| 786 |
+
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(380px, 1fr)); }
|
| 787 |
+
|
| 788 |
+
/* Responsive */
|
| 789 |
+
@media (max-width: 768px) {
|
| 790 |
+
.container { padding: 20px; }
|
| 791 |
+
.header { padding: 24px; }
|
| 792 |
+
.header-top { flex-direction: column; align-items: flex-start; }
|
| 793 |
+
.kpi-grid { grid-template-columns: 1fr; }
|
| 794 |
+
.grid-2, .grid-3 { grid-template-columns: 1fr; }
|
| 795 |
+
.tabs { overflow-x: scroll; }
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
/* Icon Styles */
|
| 799 |
+
.icon {
|
| 800 |
+
stroke: currentColor;
|
| 801 |
+
stroke-width: 2.5;
|
| 802 |
+
stroke-linecap: round;
|
| 803 |
+
stroke-linejoin: round;
|
| 804 |
+
fill: none;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.icon-lg {
|
| 808 |
+
width: 32px;
|
| 809 |
+
height: 32px;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
/* Provider Status Icons */
|
| 813 |
+
.provider-icon {
|
| 814 |
+
width: 48px;
|
| 815 |
+
height: 48px;
|
| 816 |
+
border-radius: 12px;
|
| 817 |
+
display: flex;
|
| 818 |
+
align-items: center;
|
| 819 |
+
justify-content: center;
|
| 820 |
+
font-size: 24px;
|
| 821 |
+
box-shadow: var(--shadow);
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.provider-icon.online {
|
| 825 |
+
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));
|
| 826 |
+
border: 2px solid var(--success);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.provider-icon.offline {
|
| 830 |
+
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1));
|
| 831 |
+
border: 2px solid var(--danger);
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
.provider-icon.degraded {
|
| 835 |
+
background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.1));
|
| 836 |
+
border: 2px solid var(--warning);
|
| 837 |
+
}
|
| 838 |
+
</style>
|
| 839 |
+
</head>
|
| 840 |
+
<body>
|
| 841 |
+
<div class="loading-overlay" id="loadingOverlay">
|
| 842 |
+
<div class="spinner"></div>
|
| 843 |
+
</div>
|
| 844 |
+
|
| 845 |
+
<div class="toast-container" id="toastContainer"></div>
|
| 846 |
+
|
| 847 |
+
<div class="container">
|
| 848 |
+
<!-- Header -->
|
| 849 |
+
<div class="header">
|
| 850 |
+
<div class="header-top">
|
| 851 |
+
<div class="logo">
|
| 852 |
+
<div class="logo-icon">
|
| 853 |
+
<svg class="icon icon-lg" style="stroke: white; width: 40px; height: 40px;">
|
| 854 |
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path>
|
| 855 |
+
</svg>
|
| 856 |
+
</div>
|
| 857 |
+
<div class="logo-text">
|
| 858 |
+
<h1>🚀 Crypto API Monitor</h1>
|
| 859 |
+
<p>Real-time Cryptocurrency API Resource Monitoring</p>
|
| 860 |
+
</div>
|
| 861 |
+
</div>
|
| 862 |
+
<div class="header-actions">
|
| 863 |
+
<div class="connection-status" id="wsStatus">
|
| 864 |
+
<span class="status-dot"></span>
|
| 865 |
+
<span id="wsStatusText">Connecting...</span>
|
| 866 |
+
</div>
|
| 867 |
+
<div class="status-badge" id="systemStatus">
|
| 868 |
+
<span class="status-dot"></span>
|
| 869 |
+
<span id="systemStatusText">System Active</span>
|
| 870 |
+
</div>
|
| 871 |
+
<button class="btn" onclick="refreshAll()">
|
| 872 |
+
<svg class="icon">
|
| 873 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 874 |
+
</svg>
|
| 875 |
+
Refresh All
|
| 876 |
+
</button>
|
| 877 |
+
</div>
|
| 878 |
+
</div>
|
| 879 |
+
|
| 880 |
+
<!-- KPI Cards -->
|
| 881 |
+
<div class="kpi-grid" id="kpiGrid">
|
| 882 |
+
<div class="kpi-card">
|
| 883 |
+
<div class="kpi-header">
|
| 884 |
+
<span class="kpi-label">📊 Total APIs</span>
|
| 885 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(0, 212, 255, 0.2), rgba(0, 212, 255, 0.1));">
|
| 886 |
+
<svg class="icon icon-lg" style="stroke: var(--accent-blue); width: 36px; height: 36px;">
|
| 887 |
+
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
| 888 |
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
| 889 |
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
| 890 |
+
</svg>
|
| 891 |
+
</div>
|
| 892 |
+
</div>
|
| 893 |
+
<div class="kpi-value" id="kpiTotalAPIs">--</div>
|
| 894 |
+
<div class="kpi-trend trend-neutral">
|
| 895 |
+
<svg class="icon" style="width: 18px; height: 18px;">
|
| 896 |
+
<path d="M12 20V10M18 20V4M6 20v-4"></path>
|
| 897 |
+
</svg>
|
| 898 |
+
<span id="kpiTotalTrend">Loading...</span>
|
| 899 |
+
</div>
|
| 900 |
+
</div>
|
| 901 |
+
|
| 902 |
+
<div class="kpi-card">
|
| 903 |
+
<div class="kpi-header">
|
| 904 |
+
<span class="kpi-label">✅ Online</span>
|
| 905 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(16, 185, 129, 0.1));">
|
| 906 |
+
<svg class="icon icon-lg" style="stroke: var(--success); width: 36px; height: 36px;">
|
| 907 |
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
| 908 |
+
<polyline points="9 12 11 14 15 10"></polyline>
|
| 909 |
+
</svg>
|
| 910 |
+
</div>
|
| 911 |
+
</div>
|
| 912 |
+
<div class="kpi-value" id="kpiOnline">--</div>
|
| 913 |
+
<div class="kpi-trend trend-up">
|
| 914 |
+
<svg class="icon" style="width: 18px; height: 18px;">
|
| 915 |
+
<line x1="12" y1="19" x2="12" y2="5"></line>
|
| 916 |
+
<polyline points="5 12 12 5 19 12"></polyline>
|
| 917 |
+
</svg>
|
| 918 |
+
<span id="kpiOnlineTrend">Loading...</span>
|
| 919 |
+
</div>
|
| 920 |
+
</div>
|
| 921 |
+
|
| 922 |
+
<div class="kpi-card">
|
| 923 |
+
<div class="kpi-header">
|
| 924 |
+
<span class="kpi-label">⚡ Avg Response</span>
|
| 925 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(251, 191, 36, 0.1));">
|
| 926 |
+
<svg class="icon icon-lg" style="stroke: var(--warning); width: 36px; height: 36px;">
|
| 927 |
+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
| 928 |
+
</svg>
|
| 929 |
+
</div>
|
| 930 |
+
</div>
|
| 931 |
+
<div class="kpi-value" id="kpiAvgResponse" style="font-size: 36px;">--</div>
|
| 932 |
+
<div class="kpi-trend trend-down">
|
| 933 |
+
<svg class="icon" style="width: 18px; height: 18px;">
|
| 934 |
+
<line x1="12" y1="5" x2="12" y2="19"></line>
|
| 935 |
+
<polyline points="19 12 12 19 5 12"></polyline>
|
| 936 |
+
</svg>
|
| 937 |
+
<span id="kpiResponseTrend">Loading...</span>
|
| 938 |
+
</div>
|
| 939 |
+
</div>
|
| 940 |
+
|
| 941 |
+
<div class="kpi-card">
|
| 942 |
+
<div class="kpi-header">
|
| 943 |
+
<span class="kpi-label">🕐 Last Update</span>
|
| 944 |
+
<div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.1));">
|
| 945 |
+
<svg class="icon icon-lg" style="stroke: var(--accent-purple); width: 36px; height: 36px;">
|
| 946 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 947 |
+
<polyline points="12 6 12 12 16 14"></polyline>
|
| 948 |
+
</svg>
|
| 949 |
+
</div>
|
| 950 |
+
</div>
|
| 951 |
+
<div class="kpi-value" id="kpiLastUpdate" style="font-size: 22px; line-height: 1.3;">--</div>
|
| 952 |
+
<div class="kpi-trend trend-neutral">
|
| 953 |
+
<svg class="icon" style="width: 18px; height: 18px;">
|
| 954 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 955 |
+
</svg>
|
| 956 |
+
<span>Auto-refresh</span>
|
| 957 |
+
</div>
|
| 958 |
+
</div>
|
| 959 |
+
</div>
|
| 960 |
+
</div>
|
| 961 |
+
|
| 962 |
+
<!-- Tabs -->
|
| 963 |
+
<div class="tabs">
|
| 964 |
+
<div class="tab active" onclick="switchTab(event, 'dashboard')">
|
| 965 |
+
<svg class="icon">
|
| 966 |
+
<rect x="3" y="3" width="7" height="7"></rect>
|
| 967 |
+
<rect x="14" y="3" width="7" height="7"></rect>
|
| 968 |
+
<rect x="14" y="14" width="7" height="7"></rect>
|
| 969 |
+
<rect x="3" y="14" width="7" height="7"></rect>
|
| 970 |
+
</svg>
|
| 971 |
+
<span>Dashboard</span>
|
| 972 |
+
</div>
|
| 973 |
+
<div class="tab" onclick="switchTab(event, 'providers')">
|
| 974 |
+
<svg class="icon">
|
| 975 |
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
| 976 |
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
| 977 |
+
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
| 978 |
+
</svg>
|
| 979 |
+
<span>Providers</span>
|
| 980 |
+
</div>
|
| 981 |
+
<div class="tab" onclick="switchTab(event, 'categories')">
|
| 982 |
+
<svg class="icon">
|
| 983 |
+
<rect x="3" y="3" width="7" height="7"></rect>
|
| 984 |
+
<rect x="14" y="3" width="7" height="7"></rect>
|
| 985 |
+
<rect x="14" y="14" width="7" height="7"></rect>
|
| 986 |
+
<rect x="3" y="14" width="7" height="7"></rect>
|
| 987 |
+
</svg>
|
| 988 |
+
<span>Categories</span>
|
| 989 |
+
</div>
|
| 990 |
+
<div class="tab" onclick="switchTab(event, 'logs')">
|
| 991 |
+
<svg class="icon">
|
| 992 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 993 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 994 |
+
<line x1="16" y1="13" x2="8" y2="13"></line>
|
| 995 |
+
<line x1="16" y1="17" x2="8" y2="17"></line>
|
| 996 |
+
<polyline points="10 9 9 9 8 9"></polyline>
|
| 997 |
+
</svg>
|
| 998 |
+
<span>Logs</span>
|
| 999 |
+
</div>
|
| 1000 |
+
<div class="tab" onclick="switchTab(event, 'huggingface')">
|
| 1001 |
+
<svg class="icon">
|
| 1002 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1003 |
+
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
| 1004 |
+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
| 1005 |
+
</svg>
|
| 1006 |
+
<span>🤗 HuggingFace</span>
|
| 1007 |
+
</div>
|
| 1008 |
+
</div>
|
| 1009 |
+
|
| 1010 |
+
<!-- Dashboard Tab -->
|
| 1011 |
+
<div class="tab-content active" id="tab-dashboard">
|
| 1012 |
+
<div class="card">
|
| 1013 |
+
<div class="card-header">
|
| 1014 |
+
<h2 class="card-title">
|
| 1015 |
+
<svg class="icon icon-lg">
|
| 1016 |
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
|
| 1017 |
+
</svg>
|
| 1018 |
+
System Overview
|
| 1019 |
+
</h2>
|
| 1020 |
+
<button class="btn" onclick="loadProviders()" style="padding: 10px 20px; font-size: 13px;">
|
| 1021 |
+
<svg class="icon">
|
| 1022 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1023 |
+
</svg>
|
| 1024 |
+
Refresh
|
| 1025 |
+
</button>
|
| 1026 |
+
</div>
|
| 1027 |
+
<div class="table-container">
|
| 1028 |
+
<table class="table">
|
| 1029 |
+
<thead>
|
| 1030 |
+
<tr>
|
| 1031 |
+
<th>🔌 Provider</th>
|
| 1032 |
+
<th>📁 Category</th>
|
| 1033 |
+
<th>📊 Status</th>
|
| 1034 |
+
<th>⚡ Response Time</th>
|
| 1035 |
+
<th>🕐 Last Check</th>
|
| 1036 |
+
</tr>
|
| 1037 |
+
</thead>
|
| 1038 |
+
<tbody id="providersTableBody">
|
| 1039 |
+
<tr>
|
| 1040 |
+
<td colspan="5" style="text-align: center; padding: 60px;">
|
| 1041 |
+
<div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
|
| 1042 |
+
<div style="color: var(--text-muted);">Loading providers...</div>
|
| 1043 |
+
</td>
|
| 1044 |
+
</tr>
|
| 1045 |
+
</tbody>
|
| 1046 |
+
</table>
|
| 1047 |
+
</div>
|
| 1048 |
+
</div>
|
| 1049 |
+
|
| 1050 |
+
<div class="grid grid-2">
|
| 1051 |
+
<div class="card">
|
| 1052 |
+
<div class="card-header">
|
| 1053 |
+
<h2 class="card-title">
|
| 1054 |
+
<svg class="icon icon-lg">
|
| 1055 |
+
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
|
| 1056 |
+
<polyline points="17 6 23 6 23 12"></polyline>
|
| 1057 |
+
</svg>
|
| 1058 |
+
Health Status
|
| 1059 |
+
</h2>
|
| 1060 |
+
</div>
|
| 1061 |
+
<div style="position: relative; height: 320px; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
|
| 1062 |
+
<canvas id="healthChart"></canvas>
|
| 1063 |
+
</div>
|
| 1064 |
+
</div>
|
| 1065 |
+
|
| 1066 |
+
<div class="card">
|
| 1067 |
+
<div class="card-header">
|
| 1068 |
+
<h2 class="card-title">
|
| 1069 |
+
<svg class="icon icon-lg">
|
| 1070 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1071 |
+
<line x1="2" y1="12" x2="22" y2="12"></line>
|
| 1072 |
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
| 1073 |
+
</svg>
|
| 1074 |
+
Status Distribution
|
| 1075 |
+
</h2>
|
| 1076 |
+
</div>
|
| 1077 |
+
<div style="position: relative; height: 320px; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
|
| 1078 |
+
<canvas id="statusChart"></canvas>
|
| 1079 |
+
</div>
|
| 1080 |
+
</div>
|
| 1081 |
+
</div>
|
| 1082 |
+
</div>
|
| 1083 |
+
|
| 1084 |
+
<!-- Providers Tab -->
|
| 1085 |
+
<div class="tab-content" id="tab-providers">
|
| 1086 |
+
<div class="card">
|
| 1087 |
+
<div class="card-header">
|
| 1088 |
+
<h2 class="card-title">
|
| 1089 |
+
<svg class="icon icon-lg">
|
| 1090 |
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
| 1091 |
+
</svg>
|
| 1092 |
+
All Providers
|
| 1093 |
+
</h2>
|
| 1094 |
+
<button class="btn" onclick="loadProviders()" style="padding: 10px 20px; font-size: 13px;">
|
| 1095 |
+
<svg class="icon">
|
| 1096 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1097 |
+
</svg>
|
| 1098 |
+
Refresh
|
| 1099 |
+
</button>
|
| 1100 |
+
</div>
|
| 1101 |
+
<div id="providersDetail">
|
| 1102 |
+
<div style="text-align: center; padding: 60px;">
|
| 1103 |
+
<div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
|
| 1104 |
+
<div style="color: var(--text-muted);">Loading providers details...</div>
|
| 1105 |
+
</div>
|
| 1106 |
+
</div>
|
| 1107 |
+
</div>
|
| 1108 |
+
</div>
|
| 1109 |
+
|
| 1110 |
+
<!-- Categories Tab -->
|
| 1111 |
+
<div class="tab-content" id="tab-categories">
|
| 1112 |
+
<div class="card">
|
| 1113 |
+
<div class="card-header">
|
| 1114 |
+
<h2 class="card-title">
|
| 1115 |
+
<svg class="icon icon-lg">
|
| 1116 |
+
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
|
| 1117 |
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
| 1118 |
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
| 1119 |
+
</svg>
|
| 1120 |
+
Categories Overview
|
| 1121 |
+
</h2>
|
| 1122 |
+
<button class="btn" onclick="loadCategories()" style="padding: 10px 20px; font-size: 13px;">
|
| 1123 |
+
<svg class="icon">
|
| 1124 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1125 |
+
</svg>
|
| 1126 |
+
Refresh
|
| 1127 |
+
</button>
|
| 1128 |
+
</div>
|
| 1129 |
+
<div class="table-container">
|
| 1130 |
+
<table class="table">
|
| 1131 |
+
<thead>
|
| 1132 |
+
<tr>
|
| 1133 |
+
<th>📁 Category</th>
|
| 1134 |
+
<th>📊 Total Sources</th>
|
| 1135 |
+
<th>✅ Online</th>
|
| 1136 |
+
<th>💚 Health %</th>
|
| 1137 |
+
<th>⚡ Avg Response</th>
|
| 1138 |
+
<th>🕐 Last Updated</th>
|
| 1139 |
+
<th>📈 Status</th>
|
| 1140 |
+
</tr>
|
| 1141 |
+
</thead>
|
| 1142 |
+
<tbody id="categoriesTableBody">
|
| 1143 |
+
<tr>
|
| 1144 |
+
<td colspan="7" style="text-align: center; padding: 60px;">
|
| 1145 |
+
<div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
|
| 1146 |
+
<div style="color: var(--text-muted);">Loading categories...</div>
|
| 1147 |
+
</td>
|
| 1148 |
+
</tr>
|
| 1149 |
+
</tbody>
|
| 1150 |
+
</table>
|
| 1151 |
+
</div>
|
| 1152 |
+
</div>
|
| 1153 |
+
</div>
|
| 1154 |
+
|
| 1155 |
+
<!-- Logs Tab -->
|
| 1156 |
+
<div class="tab-content" id="tab-logs">
|
| 1157 |
+
<div class="card">
|
| 1158 |
+
<div class="card-header">
|
| 1159 |
+
<h2 class="card-title">
|
| 1160 |
+
<svg class="icon icon-lg">
|
| 1161 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 1162 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 1163 |
+
</svg>
|
| 1164 |
+
Connection Logs
|
| 1165 |
+
</h2>
|
| 1166 |
+
<button class="btn" onclick="loadLogs()" style="padding: 10px 20px; font-size: 13px;">
|
| 1167 |
+
<svg class="icon">
|
| 1168 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1169 |
+
</svg>
|
| 1170 |
+
Refresh
|
| 1171 |
+
</button>
|
| 1172 |
+
</div>
|
| 1173 |
+
<div class="table-container">
|
| 1174 |
+
<table class="table">
|
| 1175 |
+
<thead>
|
| 1176 |
+
<tr>
|
| 1177 |
+
<th>🕐 Timestamp</th>
|
| 1178 |
+
<th>🔌 Provider</th>
|
| 1179 |
+
<th>📝 Type</th>
|
| 1180 |
+
<th>📊 Status</th>
|
| 1181 |
+
<th>⚡ Response Time</th>
|
| 1182 |
+
<th>💬 Message</th>
|
| 1183 |
+
</tr>
|
| 1184 |
+
</thead>
|
| 1185 |
+
<tbody id="logsTableBody">
|
| 1186 |
+
<tr>
|
| 1187 |
+
<td colspan="6" style="text-align: center; padding: 60px;">
|
| 1188 |
+
<div class="spinner" style="width: 40px; height: 40px; margin: 0 auto 20px;"></div>
|
| 1189 |
+
<div style="color: var(--text-muted);">Loading logs...</div>
|
| 1190 |
+
</td>
|
| 1191 |
+
</tr>
|
| 1192 |
+
</tbody>
|
| 1193 |
+
</table>
|
| 1194 |
+
</div>
|
| 1195 |
+
</div>
|
| 1196 |
+
</div>
|
| 1197 |
+
|
| 1198 |
+
<!-- HuggingFace Tab -->
|
| 1199 |
+
<div class="tab-content" id="tab-huggingface">
|
| 1200 |
+
<div class="card">
|
| 1201 |
+
<div class="card-header">
|
| 1202 |
+
<h2 class="card-title">
|
| 1203 |
+
<svg class="icon icon-lg">
|
| 1204 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1205 |
+
</svg>
|
| 1206 |
+
🤗 HuggingFace Health Status
|
| 1207 |
+
</h2>
|
| 1208 |
+
<button class="btn" onclick="refreshHFRegistry()" style="padding: 10px 20px; font-size: 13px;">
|
| 1209 |
+
<svg class="icon">
|
| 1210 |
+
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"></path>
|
| 1211 |
+
</svg>
|
| 1212 |
+
Refresh Registry
|
| 1213 |
+
</button>
|
| 1214 |
+
</div>
|
| 1215 |
+
<div id="hfHealthDisplay" style="padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); font-family: 'Courier New', monospace; font-size: 14px; white-space: pre-wrap; max-height: 320px; overflow-y: auto; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
|
| 1216 |
+
Loading HF health status...
|
| 1217 |
+
</div>
|
| 1218 |
+
</div>
|
| 1219 |
+
|
| 1220 |
+
<div class="grid grid-2">
|
| 1221 |
+
<div class="card">
|
| 1222 |
+
<div class="card-header">
|
| 1223 |
+
<h2 class="card-title">
|
| 1224 |
+
🤖 Models Registry
|
| 1225 |
+
<span class="badge badge-success" id="hfModelsCount">0</span>
|
| 1226 |
+
</h2>
|
| 1227 |
+
</div>
|
| 1228 |
+
<div id="hfModelsList" style="max-height: 450px; overflow-y: auto;">
|
| 1229 |
+
<div style="text-align: center; padding: 40px;">
|
| 1230 |
+
<div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 16px;"></div>
|
| 1231 |
+
<div style="color: var(--text-muted);">Loading models...</div>
|
| 1232 |
+
</div>
|
| 1233 |
+
</div>
|
| 1234 |
+
</div>
|
| 1235 |
+
|
| 1236 |
+
<div class="card">
|
| 1237 |
+
<div class="card-header">
|
| 1238 |
+
<h2 class="card-title">
|
| 1239 |
+
📊 Datasets Registry
|
| 1240 |
+
<span class="badge badge-success" id="hfDatasetsCount">0</span>
|
| 1241 |
+
</h2>
|
| 1242 |
+
</div>
|
| 1243 |
+
<div id="hfDatasetsList" style="max-height: 450px; overflow-y: auto;">
|
| 1244 |
+
<div style="text-align: center; padding: 40px;">
|
| 1245 |
+
<div class="spinner" style="width: 32px; height: 32px; margin: 0 auto 16px;"></div>
|
| 1246 |
+
<div style="color: var(--text-muted);">Loading datasets...</div>
|
| 1247 |
+
</div>
|
| 1248 |
+
</div>
|
| 1249 |
+
</div>
|
| 1250 |
+
</div>
|
| 1251 |
+
|
| 1252 |
+
<div class="card">
|
| 1253 |
+
<div class="card-header">
|
| 1254 |
+
<h2 class="card-title">
|
| 1255 |
+
<svg class="icon icon-lg">
|
| 1256 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 1257 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 1258 |
+
</svg>
|
| 1259 |
+
Search Registry
|
| 1260 |
+
</h2>
|
| 1261 |
+
</div>
|
| 1262 |
+
<div style="display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap;">
|
| 1263 |
+
<input type="text" id="hfSearchQuery" placeholder="Search crypto, bitcoin, sentiment..."
|
| 1264 |
+
style="flex: 1; min-width: 250px; padding: 14px 20px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px;"
|
| 1265 |
+
value="crypto">
|
| 1266 |
+
<select id="hfSearchKind" style="padding: 14px 20px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px;">
|
| 1267 |
+
<option value="models">Models</option>
|
| 1268 |
+
<option value="datasets">Datasets</option>
|
| 1269 |
+
</select>
|
| 1270 |
+
<button class="btn" onclick="searchHF()" style="padding: 14px 28px;">
|
| 1271 |
+
<svg class="icon">
|
| 1272 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 1273 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 1274 |
+
</svg>
|
| 1275 |
+
Search
|
| 1276 |
+
</button>
|
| 1277 |
+
</div>
|
| 1278 |
+
<div id="hfSearchResults" style="max-height: 450px; overflow-y: auto; padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
|
| 1279 |
+
<div style="text-align: center; color: var(--text-muted);">Enter a query and click search</div>
|
| 1280 |
+
</div>
|
| 1281 |
+
</div>
|
| 1282 |
+
|
| 1283 |
+
<div class="card">
|
| 1284 |
+
<div class="card-header">
|
| 1285 |
+
<h2 class="card-title">💭 Sentiment Analysis</h2>
|
| 1286 |
+
</div>
|
| 1287 |
+
<div style="margin-bottom: 20px;">
|
| 1288 |
+
<label style="display: block; font-weight: 700; margin-bottom: 12px; color: var(--text-primary);">Text Samples (one per line)</label>
|
| 1289 |
+
<textarea id="hfSentimentTexts" rows="6"
|
| 1290 |
+
style="width: 100%; padding: 16px; border-radius: 12px; border: 2px solid var(--border); background: var(--bg-secondary); color: var(--text-primary); font-size: 14px; font-family: inherit; resize: vertical;"
|
| 1291 |
+
placeholder="BTC strong breakout ETH looks weak Crypto market is bullish today">BTC strong breakout
|
| 1292 |
+
ETH looks weak
|
| 1293 |
+
Crypto market is bullish today
|
| 1294 |
+
Bears are taking control
|
| 1295 |
+
Neutral market conditions</textarea>
|
| 1296 |
+
</div>
|
| 1297 |
+
<button class="btn" onclick="runHFSentiment()">
|
| 1298 |
+
<svg class="icon">
|
| 1299 |
+
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
|
| 1300 |
+
</svg>
|
| 1301 |
+
Run Sentiment Analysis
|
| 1302 |
+
</button>
|
| 1303 |
+
<div id="hfSentimentVote" style="margin: 24px 0; padding: 32px; background: var(--bg-secondary); border-radius: var(--radius); text-align: center; font-size: 40px; font-weight: 900; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
|
| 1304 |
+
<span style="color: var(--text-muted);">—</span>
|
| 1305 |
+
</div>
|
| 1306 |
+
<div id="hfSentimentResults" style="padding: 24px; background: var(--bg-secondary); border-radius: var(--radius); font-family: 'Courier New', monospace; font-size: 13px; white-space: pre-wrap; max-height: 450px; overflow-y: auto; border: 2px solid var(--border); box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);">
|
| 1307 |
+
Results will appear here...
|
| 1308 |
+
</div>
|
| 1309 |
+
</div>
|
| 1310 |
+
</div>
|
| 1311 |
+
</div>
|
| 1312 |
+
|
| 1313 |
+
<script>
|
| 1314 |
+
// Configuration
|
| 1315 |
+
const config = {
|
| 1316 |
+
apiBaseUrl: '',
|
| 1317 |
+
wsUrl: (() => {
|
| 1318 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 1319 |
+
const host = window.location.host;
|
| 1320 |
+
return `${protocol}//${host}/ws`;
|
| 1321 |
+
})(),
|
| 1322 |
+
autoRefreshInterval: 30000,
|
| 1323 |
+
maxRetries: 3
|
| 1324 |
+
};
|
| 1325 |
+
|
| 1326 |
+
// Global state
|
| 1327 |
+
let state = {
|
| 1328 |
+
ws: null,
|
| 1329 |
+
wsConnected: false,
|
| 1330 |
+
autoRefreshEnabled: true,
|
| 1331 |
+
charts: {},
|
| 1332 |
+
currentTab: 'dashboard',
|
| 1333 |
+
providers: [],
|
| 1334 |
+
categories: [],
|
| 1335 |
+
logs: [],
|
| 1336 |
+
lastUpdate: null
|
| 1337 |
+
};
|
| 1338 |
+
|
| 1339 |
+
// Initialize on page load
|
| 1340 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1341 |
+
console.log('🚀 Initializing Crypto API Monitor...');
|
| 1342 |
+
initializeWebSocket();
|
| 1343 |
+
loadInitialData();
|
| 1344 |
+
startAutoRefresh();
|
| 1345 |
+
});
|
| 1346 |
+
|
| 1347 |
+
// WebSocket Connection
|
| 1348 |
+
function initializeWebSocket() {
|
| 1349 |
+
updateWSStatus('connecting');
|
| 1350 |
+
|
| 1351 |
+
try {
|
| 1352 |
+
state.ws = new WebSocket(config.wsUrl);
|
| 1353 |
+
setupWebSocketHandlers();
|
| 1354 |
+
} catch (error) {
|
| 1355 |
+
console.error('WebSocket connection failed:', error);
|
| 1356 |
+
updateWSStatus('disconnected');
|
| 1357 |
+
}
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
function setupWebSocketHandlers() {
|
| 1361 |
+
state.ws.onopen = () => {
|
| 1362 |
+
console.log('✅ WebSocket connected');
|
| 1363 |
+
state.wsConnected = true;
|
| 1364 |
+
updateWSStatus('connected');
|
| 1365 |
+
showToast('Connected', 'Real-time data stream active', 'success');
|
| 1366 |
+
};
|
| 1367 |
+
|
| 1368 |
+
state.ws.onmessage = (event) => {
|
| 1369 |
+
try {
|
| 1370 |
+
const data = JSON.parse(event.data);
|
| 1371 |
+
handleWSMessage(data);
|
| 1372 |
+
} catch (error) {
|
| 1373 |
+
console.error('Error parsing WebSocket message:', error);
|
| 1374 |
+
}
|
| 1375 |
+
};
|
| 1376 |
+
|
| 1377 |
+
state.ws.onerror = (error) => {
|
| 1378 |
+
console.error('❌ WebSocket error:', error);
|
| 1379 |
+
updateWSStatus('disconnected');
|
| 1380 |
+
};
|
| 1381 |
+
|
| 1382 |
+
state.ws.onclose = () => {
|
| 1383 |
+
console.log('⚠️ WebSocket disconnected');
|
| 1384 |
+
state.wsConnected = false;
|
| 1385 |
+
updateWSStatus('disconnected');
|
| 1386 |
+
};
|
| 1387 |
+
}
|
| 1388 |
+
|
| 1389 |
+
function updateWSStatus(status) {
|
| 1390 |
+
const statusEl = document.getElementById('wsStatus');
|
| 1391 |
+
const textEl = document.getElementById('wsStatusText');
|
| 1392 |
+
|
| 1393 |
+
statusEl.classList.remove('connected', 'disconnected', 'connecting');
|
| 1394 |
+
statusEl.classList.add(status);
|
| 1395 |
+
|
| 1396 |
+
const statusText = {
|
| 1397 |
+
'connected': '✓ Connected',
|
| 1398 |
+
'disconnected': '✗ Disconnected',
|
| 1399 |
+
'connecting': '⟳ Connecting...'
|
| 1400 |
+
};
|
| 1401 |
+
|
| 1402 |
+
textEl.textContent = statusText[status] || 'Unknown';
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
function handleWSMessage(data) {
|
| 1406 |
+
console.log('📨 WebSocket message:', data.type);
|
| 1407 |
+
|
| 1408 |
+
switch(data.type) {
|
| 1409 |
+
case 'status_update':
|
| 1410 |
+
updateKPIs(data.data);
|
| 1411 |
+
break;
|
| 1412 |
+
case 'provider_status_change':
|
| 1413 |
+
loadProviders();
|
| 1414 |
+
break;
|
| 1415 |
+
case 'new_alert':
|
| 1416 |
+
showToast('Alert', data.data.message, 'warning');
|
| 1417 |
+
break;
|
| 1418 |
+
default:
|
| 1419 |
+
console.log('Unknown message type:', data.type);
|
| 1420 |
+
}
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
// API Calls
|
| 1424 |
+
async function apiCall(endpoint, options = {}) {
|
| 1425 |
+
try {
|
| 1426 |
+
const url = `${config.apiBaseUrl}${endpoint}`;
|
| 1427 |
+
console.log('🌐 API Call:', url);
|
| 1428 |
+
|
| 1429 |
+
const response = await fetch(url, {
|
| 1430 |
+
...options,
|
| 1431 |
+
headers: {
|
| 1432 |
+
'Content-Type': 'application/json',
|
| 1433 |
+
...options.headers
|
| 1434 |
+
}
|
| 1435 |
+
});
|
| 1436 |
+
|
| 1437 |
+
if (!response.ok) {
|
| 1438 |
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
| 1439 |
+
}
|
| 1440 |
+
|
| 1441 |
+
const data = await response.json();
|
| 1442 |
+
console.log('✅ API Response:', endpoint, data);
|
| 1443 |
+
return data;
|
| 1444 |
+
} catch (error) {
|
| 1445 |
+
console.error(`❌ API call failed: ${endpoint}`, error);
|
| 1446 |
+
showToast('API Error', `Failed: ${endpoint}`, 'error');
|
| 1447 |
+
throw error;
|
| 1448 |
+
}
|
| 1449 |
+
}
|
| 1450 |
+
|
| 1451 |
+
async function loadInitialData() {
|
| 1452 |
+
showLoading();
|
| 1453 |
+
|
| 1454 |
+
try {
|
| 1455 |
+
console.log('📊 Loading initial data...');
|
| 1456 |
+
|
| 1457 |
+
await loadProviders();
|
| 1458 |
+
|
| 1459 |
+
initializeCharts();
|
| 1460 |
+
|
| 1461 |
+
state.lastUpdate = new Date();
|
| 1462 |
+
updateLastUpdateDisplay();
|
| 1463 |
+
|
| 1464 |
+
console.log('✅ Initial data loaded successfully');
|
| 1465 |
+
showToast('Success', 'Dashboard loaded successfully', 'success');
|
| 1466 |
+
} catch (error) {
|
| 1467 |
+
console.error('❌ Error loading initial data:', error);
|
| 1468 |
+
showToast('Error', 'Failed to load initial data', 'error');
|
| 1469 |
+
} finally {
|
| 1470 |
+
hideLoading();
|
| 1471 |
+
}
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
async function loadProviders() {
|
| 1475 |
+
try {
|
| 1476 |
+
const data = await apiCall('/api/providers');
|
| 1477 |
+
state.providers = data;
|
| 1478 |
+
renderProvidersTable(data);
|
| 1479 |
+
renderProvidersDetail(data);
|
| 1480 |
+
updateStatusChart(data);
|
| 1481 |
+
updateKPIs(data);
|
| 1482 |
+
} catch (error) {
|
| 1483 |
+
console.error('Error loading providers:', error);
|
| 1484 |
+
document.getElementById('providersTableBody').innerHTML = `
|
| 1485 |
+
<tr>
|
| 1486 |
+
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 60px;">
|
| 1487 |
+
Failed to load providers. Please check if the API endpoint is available.
|
| 1488 |
+
</td>
|
| 1489 |
+
</tr>
|
| 1490 |
+
`;
|
| 1491 |
+
}
|
| 1492 |
+
}
|
| 1493 |
+
|
| 1494 |
+
async function loadCategories() {
|
| 1495 |
+
try {
|
| 1496 |
+
showLoading();
|
| 1497 |
+
const data = await apiCall('/api/categories');
|
| 1498 |
+
state.categories = data;
|
| 1499 |
+
renderCategoriesTable(data);
|
| 1500 |
+
showToast('Success', 'Categories loaded successfully', 'success');
|
| 1501 |
+
} catch (error) {
|
| 1502 |
+
console.error('Error loading categories:', error);
|
| 1503 |
+
showToast('Error', 'Failed to load categories', 'error');
|
| 1504 |
+
} finally {
|
| 1505 |
+
hideLoading();
|
| 1506 |
+
}
|
| 1507 |
+
}
|
| 1508 |
+
|
| 1509 |
+
async function loadLogs() {
|
| 1510 |
+
try {
|
| 1511 |
+
showLoading();
|
| 1512 |
+
const data = await apiCall('/api/logs');
|
| 1513 |
+
state.logs = data;
|
| 1514 |
+
renderLogsTable(data);
|
| 1515 |
+
showToast('Success', 'Logs loaded successfully', 'success');
|
| 1516 |
+
} catch (error) {
|
| 1517 |
+
console.error('Error loading logs:', error);
|
| 1518 |
+
showToast('Error', 'Failed to load logs', 'error');
|
| 1519 |
+
} finally {
|
| 1520 |
+
hideLoading();
|
| 1521 |
+
}
|
| 1522 |
+
}
|
| 1523 |
+
|
| 1524 |
+
// HuggingFace APIs
|
| 1525 |
+
async function loadHFHealth() {
|
| 1526 |
+
try {
|
| 1527 |
+
const data = await apiCall('/api/hf/health');
|
| 1528 |
+
document.getElementById('hfHealthDisplay').textContent = JSON.stringify(data, null, 2);
|
| 1529 |
+
} catch (error) {
|
| 1530 |
+
console.error('Error loading HF health:', error);
|
| 1531 |
+
document.getElementById('hfHealthDisplay').textContent = 'Error loading health status';
|
| 1532 |
+
}
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
async function refreshHFRegistry() {
|
| 1536 |
+
try {
|
| 1537 |
+
showLoading();
|
| 1538 |
+
const data = await apiCall('/api/hf/refresh', { method: 'POST' });
|
| 1539 |
+
showToast('Success', 'HF Registry refreshed', 'success');
|
| 1540 |
+
loadHFModels();
|
| 1541 |
+
loadHFDatasets();
|
| 1542 |
+
} catch (error) {
|
| 1543 |
+
console.error('Error refreshing HF registry:', error);
|
| 1544 |
+
showToast('Error', 'Failed to refresh registry', 'error');
|
| 1545 |
+
} finally {
|
| 1546 |
+
hideLoading();
|
| 1547 |
+
}
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
async function loadHFModels() {
|
| 1551 |
+
try {
|
| 1552 |
+
const data = await apiCall('/api/hf/registry?type=models');
|
| 1553 |
+
document.getElementById('hfModelsCount').textContent = data.length || 0;
|
| 1554 |
+
document.getElementById('hfModelsList').innerHTML = data.map(item => `
|
| 1555 |
+
<div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
| 1556 |
+
<div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">🤖 ${item.id || 'Unknown'}</div>
|
| 1557 |
+
<div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
|
| 1558 |
+
</div>
|
| 1559 |
+
`).join('');
|
| 1560 |
+
} catch (error) {
|
| 1561 |
+
console.error('Error loading HF models:', error);
|
| 1562 |
+
document.getElementById('hfModelsList').innerHTML = '<div style="padding: 24px; text-align: center; color: var(--text-muted);">Error loading models</div>';
|
| 1563 |
+
}
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
async function loadHFDatasets() {
|
| 1567 |
+
try {
|
| 1568 |
+
const data = await apiCall('/api/hf/registry?type=datasets');
|
| 1569 |
+
document.getElementById('hfDatasetsCount').textContent = data.length || 0;
|
| 1570 |
+
document.getElementById('hfDatasetsList').innerHTML = data.map(item => `
|
| 1571 |
+
<div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
| 1572 |
+
<div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">📊 ${item.id || 'Unknown'}</div>
|
| 1573 |
+
<div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
|
| 1574 |
+
</div>
|
| 1575 |
+
`).join('');
|
| 1576 |
+
} catch (error) {
|
| 1577 |
+
console.error('Error loading HF datasets:', error);
|
| 1578 |
+
document.getElementById('hfDatasetsList').innerHTML = '<div style="padding: 24px; text-align: center; color: var(--text-muted);">Error loading datasets</div>';
|
| 1579 |
+
}
|
| 1580 |
+
}
|
| 1581 |
+
|
| 1582 |
+
async function searchHF() {
|
| 1583 |
+
try {
|
| 1584 |
+
showLoading();
|
| 1585 |
+
const query = document.getElementById('hfSearchQuery').value;
|
| 1586 |
+
const kind = document.getElementById('hfSearchKind').value;
|
| 1587 |
+
const data = await apiCall(`/api/hf/search?q=${encodeURIComponent(query)}&kind=${kind}`);
|
| 1588 |
+
|
| 1589 |
+
document.getElementById('hfSearchResults').innerHTML = data.map(item => `
|
| 1590 |
+
<div style="padding: 16px; border-bottom: 1px solid var(--border); transition: var(--transition); cursor: pointer;" onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
| 1591 |
+
<div style="font-weight: 700; margin-bottom: 6px; color: var(--text-primary);">${kind === 'models' ? '🤖' : '📊'} ${item.id || 'Unknown'}</div>
|
| 1592 |
+
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 6px;">${item.description || 'No description'}</div>
|
| 1593 |
+
<div style="font-size: 11px; color: var(--accent-blue);">Downloads: ${item.downloads || 0} • Likes: ${item.likes || 0}</div>
|
| 1594 |
+
</div>
|
| 1595 |
+
`).join('');
|
| 1596 |
+
|
| 1597 |
+
showToast('Success', `Found ${data.length} results`, 'success');
|
| 1598 |
+
} catch (error) {
|
| 1599 |
+
console.error('Error searching HF:', error);
|
| 1600 |
+
showToast('Error', 'Search failed', 'error');
|
| 1601 |
+
} finally {
|
| 1602 |
+
hideLoading();
|
| 1603 |
+
}
|
| 1604 |
+
}
|
| 1605 |
+
|
| 1606 |
+
async function runHFSentiment() {
|
| 1607 |
+
try {
|
| 1608 |
+
showLoading();
|
| 1609 |
+
const texts = document.getElementById('hfSentimentTexts').value.split('\n').filter(t => t.trim());
|
| 1610 |
+
|
| 1611 |
+
const data = await apiCall('/api/hf/run-sentiment', {
|
| 1612 |
+
method: 'POST',
|
| 1613 |
+
body: JSON.stringify({ texts: texts })
|
| 1614 |
+
});
|
| 1615 |
+
|
| 1616 |
+
document.getElementById('hfSentimentResults').textContent = JSON.stringify(data, null, 2);
|
| 1617 |
+
|
| 1618 |
+
// Calculate overall sentiment vote
|
| 1619 |
+
const sentiments = data.results || [];
|
| 1620 |
+
const positive = sentiments.filter(s => s.sentiment === 'positive').length;
|
| 1621 |
+
const negative = sentiments.filter(s => s.sentiment === 'negative').length;
|
| 1622 |
+
const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;
|
| 1623 |
+
|
| 1624 |
+
let overall = 'NEUTRAL';
|
| 1625 |
+
let color = 'var(--info)';
|
| 1626 |
+
|
| 1627 |
+
if (positive > negative && positive > neutral) {
|
| 1628 |
+
overall = 'BULLISH 📈';
|
| 1629 |
+
color = 'var(--success)';
|
| 1630 |
+
} else if (negative > positive && negative > neutral) {
|
| 1631 |
+
overall = 'BEARISH 📉';
|
| 1632 |
+
color = 'var(--danger)';
|
| 1633 |
+
}
|
| 1634 |
+
|
| 1635 |
+
document.getElementById('hfSentimentVote').innerHTML = `
|
| 1636 |
+
<span style="color: ${color};">${overall}</span>
|
| 1637 |
+
<div style="font-size: 16px; margin-top: 12px; color: var(--text-muted);">
|
| 1638 |
+
Positive: ${positive} • Negative: ${negative} • Neutral: ${neutral}
|
| 1639 |
+
</div>
|
| 1640 |
+
`;
|
| 1641 |
+
|
| 1642 |
+
showToast('Success', 'Sentiment analysis completed', 'success');
|
| 1643 |
+
} catch (error) {
|
| 1644 |
+
console.error('Error running sentiment analysis:', error);
|
| 1645 |
+
showToast('Error', 'Sentiment analysis failed', 'error');
|
| 1646 |
+
} finally {
|
| 1647 |
+
hideLoading();
|
| 1648 |
+
}
|
| 1649 |
+
}
|
| 1650 |
+
|
| 1651 |
+
// Rendering Functions
|
| 1652 |
+
function renderProvidersTable(providers) {
|
| 1653 |
+
const tbody = document.getElementById('providersTableBody');
|
| 1654 |
+
|
| 1655 |
+
if (!providers || providers.length === 0) {
|
| 1656 |
+
tbody.innerHTML = `
|
| 1657 |
+
<tr>
|
| 1658 |
+
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 60px;">
|
| 1659 |
+
No providers found
|
| 1660 |
+
</td>
|
| 1661 |
+
</tr>
|
| 1662 |
+
`;
|
| 1663 |
+
return;
|
| 1664 |
+
}
|
| 1665 |
+
|
| 1666 |
+
tbody.innerHTML = providers.map(provider => `
|
| 1667 |
+
<tr>
|
| 1668 |
+
<td>
|
| 1669 |
+
<div style="display: flex; align-items: center; gap: 12px;">
|
| 1670 |
+
<div class="provider-icon ${provider.status || 'offline'}">
|
| 1671 |
+
${provider.status === 'online' ? '✅' : provider.status === 'degraded' ? '⚠️' : '❌'}
|
| 1672 |
+
</div>
|
| 1673 |
+
<div>
|
| 1674 |
+
<strong style="font-size: 15px;">${provider.name || 'Unknown'}</strong>
|
| 1675 |
+
<div style="font-size: 11px; color: var(--text-muted);">${provider.base_url || ''}</div>
|
| 1676 |
+
</div>
|
| 1677 |
+
</div>
|
| 1678 |
+
</td>
|
| 1679 |
+
<td>
|
| 1680 |
+
<span class="badge badge-info">${provider.category || 'General'}</span>
|
| 1681 |
+
</td>
|
| 1682 |
+
<td>
|
| 1683 |
+
<span class="badge ${getStatusBadgeClass(provider.status)}">
|
| 1684 |
+
${provider.status || 'unknown'}
|
| 1685 |
+
</span>
|
| 1686 |
+
</td>
|
| 1687 |
+
<td>
|
| 1688 |
+
<strong style="color: ${provider.response_time < 500 ? 'var(--success)' : provider.response_time < 1000 ? 'var(--warning)' : 'var(--danger)'};">
|
| 1689 |
+
${provider.response_time ? provider.response_time + 'ms' : '--'}
|
| 1690 |
+
</strong>
|
| 1691 |
+
</td>
|
| 1692 |
+
<td>
|
| 1693 |
+
<div style="font-size: 12px; font-weight: 700;">${formatTimestamp(provider.last_checked)}</div>
|
| 1694 |
+
<div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(provider.last_checked)}</div>
|
| 1695 |
+
</td>
|
| 1696 |
+
</tr>
|
| 1697 |
+
`).join('');
|
| 1698 |
+
}
|
| 1699 |
+
|
| 1700 |
+
function renderProvidersDetail(providers) {
|
| 1701 |
+
const container = document.getElementById('providersDetail');
|
| 1702 |
+
|
| 1703 |
+
if (!providers || providers.length === 0) {
|
| 1704 |
+
container.innerHTML = `
|
| 1705 |
+
<div style="text-align: center; color: var(--text-muted); padding: 60px;">
|
| 1706 |
+
No providers data available
|
| 1707 |
+
</div>
|
| 1708 |
+
`;
|
| 1709 |
+
return;
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
container.innerHTML = `
|
| 1713 |
+
<div class="table-container">
|
| 1714 |
+
<table class="table">
|
| 1715 |
+
<thead>
|
| 1716 |
+
<tr>
|
| 1717 |
+
<th>🔌 Provider</th>
|
| 1718 |
+
<th>📊 Status</th>
|
| 1719 |
+
<th>⚡ Response Time</th>
|
| 1720 |
+
<th>💚 Success Rate</th>
|
| 1721 |
+
<th>✅ Last Success</th>
|
| 1722 |
+
<th>❌ Errors (24h)</th>
|
| 1723 |
+
</tr>
|
| 1724 |
+
</thead>
|
| 1725 |
+
<tbody>
|
| 1726 |
+
${providers.map(provider => `
|
| 1727 |
+
<tr>
|
| 1728 |
+
<td>
|
| 1729 |
+
<div style="display: flex; align-items: center; gap: 12px;">
|
| 1730 |
+
<div class="provider-icon ${provider.status || 'offline'}">
|
| 1731 |
+
${provider.status === 'online' ? '✅' : provider.status === 'degraded' ? '⚠️' : '❌'}
|
| 1732 |
+
</div>
|
| 1733 |
+
<div>
|
| 1734 |
+
<strong style="font-size: 15px;">${provider.name || 'Unknown'}</strong>
|
| 1735 |
+
<div style="font-size: 11px; color: var(--text-muted);">${provider.base_url || ''}</div>
|
| 1736 |
+
</div>
|
| 1737 |
+
</div>
|
| 1738 |
+
</td>
|
| 1739 |
+
<td>
|
| 1740 |
+
<span class="badge ${getStatusBadgeClass(provider.status)}">
|
| 1741 |
+
${provider.status || 'unknown'}
|
| 1742 |
+
</span>
|
| 1743 |
+
</td>
|
| 1744 |
+
<td>
|
| 1745 |
+
<strong style="color: ${provider.response_time < 500 ? 'var(--success)' : provider.response_time < 1000 ? 'var(--warning)' : 'var(--danger)'};">
|
| 1746 |
+
${provider.response_time ? provider.response_time + 'ms' : '--'}
|
| 1747 |
+
</strong>
|
| 1748 |
+
</td>
|
| 1749 |
+
<td>
|
| 1750 |
+
<div class="progress">
|
| 1751 |
+
<div class="progress-bar ${getHealthClass(provider.success_rate || 0)}"
|
| 1752 |
+
style="width: ${provider.success_rate || 0}%"></div>
|
| 1753 |
+
</div>
|
| 1754 |
+
<small style="font-weight: 700;">${Math.round(provider.success_rate || 0)}%</small>
|
| 1755 |
+
</td>
|
| 1756 |
+
<td>${formatTimestamp(provider.last_success)}</td>
|
| 1757 |
+
<td>
|
| 1758 |
+
<span class="badge ${provider.error_count_24h > 10 ? 'badge-danger' : provider.error_count_24h > 0 ? 'badge-warning' : 'badge-success'}">
|
| 1759 |
+
${provider.error_count_24h || 0}
|
| 1760 |
+
</span>
|
| 1761 |
+
</td>
|
| 1762 |
+
</tr>
|
| 1763 |
+
`).join('')}
|
| 1764 |
+
</tbody>
|
| 1765 |
+
</table>
|
| 1766 |
+
</div>
|
| 1767 |
+
`;
|
| 1768 |
+
}
|
| 1769 |
+
|
| 1770 |
+
function renderCategoriesTable(categories) {
|
| 1771 |
+
const tbody = document.getElementById('categoriesTableBody');
|
| 1772 |
+
|
| 1773 |
+
if (!categories || categories.length === 0) {
|
| 1774 |
+
tbody.innerHTML = `
|
| 1775 |
+
<tr>
|
| 1776 |
+
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 60px;">
|
| 1777 |
+
No categories found
|
| 1778 |
+
</td>
|
| 1779 |
+
</tr>
|
| 1780 |
+
`;
|
| 1781 |
+
return;
|
| 1782 |
+
}
|
| 1783 |
+
|
| 1784 |
+
tbody.innerHTML = categories.map(category => `
|
| 1785 |
+
<tr>
|
| 1786 |
+
<td>
|
| 1787 |
+
<strong style="font-size: 15px;">📁 ${category.name || 'Unnamed'}</strong>
|
| 1788 |
+
</td>
|
| 1789 |
+
<td><strong>${category.total_sources || 0}</strong></td>
|
| 1790 |
+
<td><strong style="color: var(--success);">${category.online || 0}</strong></td>
|
| 1791 |
+
<td>
|
| 1792 |
+
<div class="progress">
|
| 1793 |
+
<div class="progress-bar ${getHealthClass(category.health_percentage || 0)}"
|
| 1794 |
+
style="width: ${category.health_percentage || 0}%"></div>
|
| 1795 |
+
</div>
|
| 1796 |
+
<small style="font-weight: 700;">${Math.round(category.health_percentage || 0)}%</small>
|
| 1797 |
+
</td>
|
| 1798 |
+
<td><strong>${category.avg_response || '--'}ms</strong></td>
|
| 1799 |
+
<td>${formatTimestamp(category.last_updated)}</td>
|
| 1800 |
+
<td>
|
| 1801 |
+
<span class="badge ${getStatusBadgeClass(category.status)}">
|
| 1802 |
+
${category.status || 'unknown'}
|
| 1803 |
+
</span>
|
| 1804 |
+
</td>
|
| 1805 |
+
</tr>
|
| 1806 |
+
`).join('');
|
| 1807 |
+
}
|
| 1808 |
+
|
| 1809 |
+
function renderLogsTable(logs) {
|
| 1810 |
+
const tbody = document.getElementById('logsTableBody');
|
| 1811 |
+
|
| 1812 |
+
if (!logs || logs.length === 0) {
|
| 1813 |
+
tbody.innerHTML = `
|
| 1814 |
+
<tr>
|
| 1815 |
+
<td colspan="6" style="text-align: center; color: var(--text-muted); padding: 60px;">
|
| 1816 |
+
No logs found
|
| 1817 |
+
</td>
|
| 1818 |
+
</tr>
|
| 1819 |
+
`;
|
| 1820 |
+
return;
|
| 1821 |
+
}
|
| 1822 |
+
|
| 1823 |
+
tbody.innerHTML = logs.map(log => `
|
| 1824 |
+
<tr>
|
| 1825 |
+
<td>
|
| 1826 |
+
<div style="font-size: 12px; font-weight: 700;">${formatTimestamp(log.timestamp)}</div>
|
| 1827 |
+
<div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(log.timestamp)}</div>
|
| 1828 |
+
</td>
|
| 1829 |
+
<td>
|
| 1830 |
+
<strong>${log.provider || 'System'}</strong>
|
| 1831 |
+
</td>
|
| 1832 |
+
<td>
|
| 1833 |
+
<span class="badge ${getLogTypeClass(log.type)}">
|
| 1834 |
+
${log.type || 'unknown'}
|
| 1835 |
+
</span>
|
| 1836 |
+
</td>
|
| 1837 |
+
<td>
|
| 1838 |
+
<span class="badge ${getStatusBadgeClass(log.status)}">
|
| 1839 |
+
${log.status || 'unknown'}
|
| 1840 |
+
</span>
|
| 1841 |
+
</td>
|
| 1842 |
+
<td><strong>${log.response_time ? log.response_time + 'ms' : '--'}</strong></td>
|
| 1843 |
+
<td>
|
| 1844 |
+
<div style="max-width: 350px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
| 1845 |
+
${log.message || 'No message'}
|
| 1846 |
+
</div>
|
| 1847 |
+
</td>
|
| 1848 |
+
</tr>
|
| 1849 |
+
`).join('');
|
| 1850 |
+
}
|
| 1851 |
+
|
| 1852 |
+
// Helper functions
|
| 1853 |
+
function getHealthClass(percentage) {
|
| 1854 |
+
if (percentage >= 80) return 'success';
|
| 1855 |
+
if (percentage >= 60) return 'warning';
|
| 1856 |
+
return 'danger';
|
| 1857 |
+
}
|
| 1858 |
+
|
| 1859 |
+
function getStatusBadgeClass(status) {
|
| 1860 |
+
switch (status?.toLowerCase()) {
|
| 1861 |
+
case 'healthy': case 'online': case 'success': return 'badge-success';
|
| 1862 |
+
case 'degraded': case 'warning': return 'badge-warning';
|
| 1863 |
+
case 'offline': case 'error': case 'critical': return 'badge-danger';
|
| 1864 |
+
default: return 'badge-info';
|
| 1865 |
+
}
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
function getLogTypeClass(type) {
|
| 1869 |
+
switch (type?.toLowerCase()) {
|
| 1870 |
+
case 'error': return 'badge-danger';
|
| 1871 |
+
case 'warning': return 'badge-warning';
|
| 1872 |
+
case 'info': case 'connection': return 'badge-info';
|
| 1873 |
+
case 'success': return 'badge-success';
|
| 1874 |
+
default: return 'badge-info';
|
| 1875 |
+
}
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
function formatTimestamp(timestamp) {
|
| 1879 |
+
if (!timestamp) return '--';
|
| 1880 |
+
try {
|
| 1881 |
+
return new Date(timestamp).toLocaleString();
|
| 1882 |
+
} catch {
|
| 1883 |
+
return 'Invalid Date';
|
| 1884 |
+
}
|
| 1885 |
+
}
|
| 1886 |
+
|
| 1887 |
+
function formatTimeAgo(timestamp) {
|
| 1888 |
+
if (!timestamp) return '';
|
| 1889 |
+
try {
|
| 1890 |
+
const now = new Date();
|
| 1891 |
+
const time = new Date(timestamp);
|
| 1892 |
+
const diff = now - time;
|
| 1893 |
+
|
| 1894 |
+
const minutes = Math.floor(diff / 60000);
|
| 1895 |
+
const hours = Math.floor(diff / 3600000);
|
| 1896 |
+
const days = Math.floor(diff / 86400000);
|
| 1897 |
+
|
| 1898 |
+
if (days > 0) return `${days}d ago`;
|
| 1899 |
+
if (hours > 0) return `${hours}h ago`;
|
| 1900 |
+
if (minutes > 0) return `${minutes}m ago`;
|
| 1901 |
+
return 'Just now';
|
| 1902 |
+
} catch {
|
| 1903 |
+
return 'Unknown';
|
| 1904 |
+
}
|
| 1905 |
+
}
|
| 1906 |
+
|
| 1907 |
+
// KPI Updates
|
| 1908 |
+
function updateKPIs(data) {
|
| 1909 |
+
if (!data) return;
|
| 1910 |
+
|
| 1911 |
+
const totalAPIs = data.length || 0;
|
| 1912 |
+
document.getElementById('kpiTotalAPIs').textContent = totalAPIs;
|
| 1913 |
+
document.getElementById('kpiTotalTrend').textContent = `${totalAPIs} active`;
|
| 1914 |
+
|
| 1915 |
+
const onlineCount = data.filter(p => p.status === 'online' || p.status === 'healthy').length;
|
| 1916 |
+
document.getElementById('kpiOnline').textContent = onlineCount;
|
| 1917 |
+
document.getElementById('kpiOnlineTrend').textContent = `${Math.round((onlineCount / totalAPIs) * 100)}% uptime`;
|
| 1918 |
+
|
| 1919 |
+
const validResponses = data.filter(p => p.response_time).map(p => p.response_time);
|
| 1920 |
+
const avgResponse = validResponses.length > 0 ?
|
| 1921 |
+
Math.round(validResponses.reduce((a, b) => a + b, 0) / validResponses.length) : 0;
|
| 1922 |
+
|
| 1923 |
+
document.getElementById('kpiAvgResponse').textContent = avgResponse + 'ms';
|
| 1924 |
+
|
| 1925 |
+
const responseTrend = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
|
| 1926 |
+
document.getElementById('kpiResponseTrend').textContent = responseTrend;
|
| 1927 |
+
}
|
| 1928 |
+
|
| 1929 |
+
function updateLastUpdateDisplay() {
|
| 1930 |
+
if (state.lastUpdate) {
|
| 1931 |
+
document.getElementById('kpiLastUpdate').textContent =
|
| 1932 |
+
state.lastUpdate.toLocaleTimeString() + '\n' + state.lastUpdate.toLocaleDateString();
|
| 1933 |
+
}
|
| 1934 |
+
}
|
| 1935 |
+
|
| 1936 |
+
// Chart Functions
|
| 1937 |
+
function initializeCharts() {
|
| 1938 |
+
const healthCtx = document.getElementById('healthChart').getContext('2d');
|
| 1939 |
+
state.charts.health = new Chart(healthCtx, {
|
| 1940 |
+
type: 'line',
|
| 1941 |
+
data: {
|
| 1942 |
+
labels: [],
|
| 1943 |
+
datasets: [{
|
| 1944 |
+
label: 'System Health %',
|
| 1945 |
+
data: [],
|
| 1946 |
+
borderColor: '#00d4ff',
|
| 1947 |
+
backgroundColor: 'rgba(0, 212, 255, 0.1)',
|
| 1948 |
+
borderWidth: 3,
|
| 1949 |
+
fill: true,
|
| 1950 |
+
tension: 0.4
|
| 1951 |
+
}]
|
| 1952 |
+
},
|
| 1953 |
+
options: {
|
| 1954 |
+
responsive: true,
|
| 1955 |
+
maintainAspectRatio: false,
|
| 1956 |
+
plugins: {
|
| 1957 |
+
legend: {
|
| 1958 |
+
display: false
|
| 1959 |
+
}
|
| 1960 |
+
},
|
| 1961 |
+
scales: {
|
| 1962 |
+
y: {
|
| 1963 |
+
beginAtZero: true,
|
| 1964 |
+
max: 100,
|
| 1965 |
+
grid: {
|
| 1966 |
+
color: 'rgba(255, 255, 255, 0.1)'
|
| 1967 |
+
},
|
| 1968 |
+
ticks: {
|
| 1969 |
+
color: '#b8c1ec'
|
| 1970 |
+
}
|
| 1971 |
+
},
|
| 1972 |
+
x: {
|
| 1973 |
+
grid: {
|
| 1974 |
+
color: 'rgba(255, 255, 255, 0.1)'
|
| 1975 |
+
},
|
| 1976 |
+
ticks: {
|
| 1977 |
+
color: '#b8c1ec'
|
| 1978 |
+
}
|
| 1979 |
+
}
|
| 1980 |
+
}
|
| 1981 |
+
}
|
| 1982 |
+
});
|
| 1983 |
+
|
| 1984 |
+
const statusCtx = document.getElementById('statusChart').getContext('2d');
|
| 1985 |
+
state.charts.status = new Chart(statusCtx, {
|
| 1986 |
+
type: 'doughnut',
|
| 1987 |
+
data: {
|
| 1988 |
+
labels: ['Online', 'Degraded', 'Offline'],
|
| 1989 |
+
datasets: [{
|
| 1990 |
+
data: [0, 0, 0],
|
| 1991 |
+
backgroundColor: [
|
| 1992 |
+
'#10b981',
|
| 1993 |
+
'#fbbf24',
|
| 1994 |
+
'#ef4444'
|
| 1995 |
+
],
|
| 1996 |
+
borderWidth: 0
|
| 1997 |
+
}]
|
| 1998 |
+
},
|
| 1999 |
+
options: {
|
| 2000 |
+
responsive: true,
|
| 2001 |
+
maintainAspectRatio: false,
|
| 2002 |
+
cutout: '70%',
|
| 2003 |
+
plugins: {
|
| 2004 |
+
legend: {
|
| 2005 |
+
position: 'bottom',
|
| 2006 |
+
labels: {
|
| 2007 |
+
color: '#b8c1ec',
|
| 2008 |
+
font: {
|
| 2009 |
+
size: 13,
|
| 2010 |
+
weight: 700
|
| 2011 |
+
}
|
| 2012 |
+
}
|
| 2013 |
+
}
|
| 2014 |
+
}
|
| 2015 |
+
}
|
| 2016 |
+
});
|
| 2017 |
+
}
|
| 2018 |
+
|
| 2019 |
+
function updateStatusChart(providers) {
|
| 2020 |
+
if (!state.charts.status || !providers) return;
|
| 2021 |
+
|
| 2022 |
+
const online = providers.filter(p => p.status === 'online' || p.status === 'healthy').length;
|
| 2023 |
+
const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'warning').length;
|
| 2024 |
+
const offline = providers.filter(p => p.status === 'offline' || p.status === 'error').length;
|
| 2025 |
+
|
| 2026 |
+
state.charts.status.data.datasets[0].data = [online, degraded, offline];
|
| 2027 |
+
state.charts.status.update();
|
| 2028 |
+
}
|
| 2029 |
+
|
| 2030 |
+
// Tab Management
|
| 2031 |
+
function switchTab(event, tabName) {
|
| 2032 |
+
document.querySelectorAll('.tab').forEach(tab => {
|
| 2033 |
+
tab.classList.remove('active');
|
| 2034 |
+
});
|
| 2035 |
+
|
| 2036 |
+
document.querySelectorAll('.tab-content').forEach(content => {
|
| 2037 |
+
content.classList.remove('active');
|
| 2038 |
+
});
|
| 2039 |
+
|
| 2040 |
+
event.currentTarget.classList.add('active');
|
| 2041 |
+
|
| 2042 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 2043 |
+
|
| 2044 |
+
switch(tabName) {
|
| 2045 |
+
case 'dashboard':
|
| 2046 |
+
loadProviders();
|
| 2047 |
+
break;
|
| 2048 |
+
case 'providers':
|
| 2049 |
+
loadProviders();
|
| 2050 |
+
break;
|
| 2051 |
+
case 'categories':
|
| 2052 |
+
loadCategories();
|
| 2053 |
+
break;
|
| 2054 |
+
case 'logs':
|
| 2055 |
+
loadLogs();
|
| 2056 |
+
break;
|
| 2057 |
+
case 'huggingface':
|
| 2058 |
+
loadHFHealth();
|
| 2059 |
+
loadHFModels();
|
| 2060 |
+
loadHFDatasets();
|
| 2061 |
+
break;
|
| 2062 |
+
}
|
| 2063 |
+
|
| 2064 |
+
state.currentTab = tabName;
|
| 2065 |
+
}
|
| 2066 |
+
|
| 2067 |
+
// Utility Functions
|
| 2068 |
+
function showLoading() {
|
| 2069 |
+
document.getElementById('loadingOverlay').classList.add('active');
|
| 2070 |
+
}
|
| 2071 |
+
|
| 2072 |
+
function hideLoading() {
|
| 2073 |
+
document.getElementById('loadingOverlay').classList.remove('active');
|
| 2074 |
+
}
|
| 2075 |
+
|
| 2076 |
+
function showToast(title, message, type = 'info') {
|
| 2077 |
+
const container = document.getElementById('toastContainer');
|
| 2078 |
+
const toast = document.createElement('div');
|
| 2079 |
+
toast.className = `toast ${type}`;
|
| 2080 |
+
toast.innerHTML = `
|
| 2081 |
+
<div style="flex: 1;">
|
| 2082 |
+
<div style="font-weight: 700; font-size: 15px; margin-bottom: 4px;">${title}</div>
|
| 2083 |
+
<div style="font-size: 13px; color: var(--text-secondary);">${message}</div>
|
| 2084 |
+
</div>
|
| 2085 |
+
<button onclick="this.parentElement.remove()" style="background: none; border: none; cursor: pointer; color: inherit; padding: 4px;">
|
| 2086 |
+
<svg class="icon" style="width: 18px; height: 18px;">
|
| 2087 |
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
| 2088 |
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
| 2089 |
+
</svg>
|
| 2090 |
+
</button>
|
| 2091 |
+
`;
|
| 2092 |
+
|
| 2093 |
+
container.appendChild(toast);
|
| 2094 |
+
|
| 2095 |
+
setTimeout(() => {
|
| 2096 |
+
if (toast.parentElement) {
|
| 2097 |
+
toast.remove();
|
| 2098 |
+
}
|
| 2099 |
+
}, 5000);
|
| 2100 |
+
}
|
| 2101 |
+
|
| 2102 |
+
function refreshAll() {
|
| 2103 |
+
console.log('🔄 Refreshing all data...');
|
| 2104 |
+
loadInitialData();
|
| 2105 |
+
|
| 2106 |
+
switch(state.currentTab) {
|
| 2107 |
+
case 'categories':
|
| 2108 |
+
loadCategories();
|
| 2109 |
+
break;
|
| 2110 |
+
case 'logs':
|
| 2111 |
+
loadLogs();
|
| 2112 |
+
break;
|
| 2113 |
+
case 'huggingface':
|
| 2114 |
+
loadHFHealth();
|
| 2115 |
+
loadHFModels();
|
| 2116 |
+
loadHFDatasets();
|
| 2117 |
+
break;
|
| 2118 |
+
}
|
| 2119 |
+
}
|
| 2120 |
+
|
| 2121 |
+
function startAutoRefresh() {
|
| 2122 |
+
setInterval(() => {
|
| 2123 |
+
if (state.autoRefreshEnabled && state.wsConnected) {
|
| 2124 |
+
console.log('🔄 Auto-refreshing data...');
|
| 2125 |
+
refreshAll();
|
| 2126 |
+
}
|
| 2127 |
+
}, config.autoRefreshInterval);
|
| 2128 |
+
}
|
| 2129 |
+
</script>
|
| 2130 |
+
</body>
|
| 2131 |
+
</html>
|
| 2132 |
+
|
archive_html/pool_management.html
ADDED
|
@@ -0,0 +1,765 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Source Pool Management - Crypto API Monitor</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--bg-primary: #f8fafc;
|
| 11 |
+
--bg-secondary: #ffffff;
|
| 12 |
+
--bg-card: #ffffff;
|
| 13 |
+
--bg-hover: #f1f5f9;
|
| 14 |
+
--text-primary: #0f172a;
|
| 15 |
+
--text-secondary: #475569;
|
| 16 |
+
--text-muted: #94a3b8;
|
| 17 |
+
--accent-primary: #3b82f6;
|
| 18 |
+
--accent-secondary: #8b5cf6;
|
| 19 |
+
--success: #10b981;
|
| 20 |
+
--success-bg: #d1fae5;
|
| 21 |
+
--warning: #f59e0b;
|
| 22 |
+
--warning-bg: #fef3c7;
|
| 23 |
+
--danger: #ef4444;
|
| 24 |
+
--danger-bg: #fee2e2;
|
| 25 |
+
--info: #06b6d4;
|
| 26 |
+
--info-bg: #cffafe;
|
| 27 |
+
--border: #e2e8f0;
|
| 28 |
+
--shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
| 29 |
+
--radius: 12px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
* {
|
| 33 |
+
margin: 0;
|
| 34 |
+
padding: 0;
|
| 35 |
+
box-sizing: border-box;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
body {
|
| 39 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 40 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 41 |
+
color: var(--text-primary);
|
| 42 |
+
line-height: 1.6;
|
| 43 |
+
min-height: 100vh;
|
| 44 |
+
padding: 20px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.container {
|
| 48 |
+
max-width: 1600px;
|
| 49 |
+
margin: 0 auto;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.header {
|
| 53 |
+
background: var(--bg-secondary);
|
| 54 |
+
border-radius: var(--radius);
|
| 55 |
+
padding: 24px;
|
| 56 |
+
margin-bottom: 24px;
|
| 57 |
+
box-shadow: var(--shadow);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.header h1 {
|
| 61 |
+
font-size: 28px;
|
| 62 |
+
font-weight: 800;
|
| 63 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 64 |
+
-webkit-background-clip: text;
|
| 65 |
+
-webkit-text-fill-color: transparent;
|
| 66 |
+
margin-bottom: 8px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.header p {
|
| 70 |
+
color: var(--text-muted);
|
| 71 |
+
font-size: 14px;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.header-actions {
|
| 75 |
+
display: flex;
|
| 76 |
+
gap: 12px;
|
| 77 |
+
margin-top: 16px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.btn {
|
| 81 |
+
padding: 10px 20px;
|
| 82 |
+
border-radius: 8px;
|
| 83 |
+
border: none;
|
| 84 |
+
font-weight: 600;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
transition: all 0.3s;
|
| 87 |
+
font-size: 14px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn-primary {
|
| 91 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 92 |
+
color: white;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.btn-primary:hover {
|
| 96 |
+
transform: translateY(-2px);
|
| 97 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.btn-secondary {
|
| 101 |
+
background: var(--bg-hover);
|
| 102 |
+
color: var(--text-primary);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.btn-danger {
|
| 106 |
+
background: var(--danger);
|
| 107 |
+
color: white;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.btn-sm {
|
| 111 |
+
padding: 6px 12px;
|
| 112 |
+
font-size: 12px;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.pools-grid {
|
| 116 |
+
display: grid;
|
| 117 |
+
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
| 118 |
+
gap: 20px;
|
| 119 |
+
margin-bottom: 24px;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.pool-card {
|
| 123 |
+
background: var(--bg-card);
|
| 124 |
+
border-radius: var(--radius);
|
| 125 |
+
padding: 20px;
|
| 126 |
+
box-shadow: var(--shadow);
|
| 127 |
+
transition: all 0.3s;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.pool-card:hover {
|
| 131 |
+
transform: translateY(-4px);
|
| 132 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.pool-header {
|
| 136 |
+
display: flex;
|
| 137 |
+
justify-content: space-between;
|
| 138 |
+
align-items: flex-start;
|
| 139 |
+
margin-bottom: 16px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.pool-title {
|
| 143 |
+
font-size: 18px;
|
| 144 |
+
font-weight: 700;
|
| 145 |
+
color: var(--text-primary);
|
| 146 |
+
margin-bottom: 4px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.pool-category {
|
| 150 |
+
display: inline-block;
|
| 151 |
+
padding: 4px 12px;
|
| 152 |
+
border-radius: 999px;
|
| 153 |
+
background: var(--info-bg);
|
| 154 |
+
color: var(--info);
|
| 155 |
+
font-size: 12px;
|
| 156 |
+
font-weight: 600;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.pool-stats {
|
| 160 |
+
display: grid;
|
| 161 |
+
grid-template-columns: repeat(2, 1fr);
|
| 162 |
+
gap: 12px;
|
| 163 |
+
margin: 16px 0;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.stat-item {
|
| 167 |
+
background: var(--bg-hover);
|
| 168 |
+
padding: 12px;
|
| 169 |
+
border-radius: 8px;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.stat-label {
|
| 173 |
+
font-size: 12px;
|
| 174 |
+
color: var(--text-muted);
|
| 175 |
+
margin-bottom: 4px;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.stat-value {
|
| 179 |
+
font-size: 20px;
|
| 180 |
+
font-weight: 700;
|
| 181 |
+
color: var(--text-primary);
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.pool-members {
|
| 185 |
+
margin-top: 16px;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.member-item {
|
| 189 |
+
display: flex;
|
| 190 |
+
justify-content: space-between;
|
| 191 |
+
align-items: center;
|
| 192 |
+
padding: 10px;
|
| 193 |
+
background: var(--bg-hover);
|
| 194 |
+
border-radius: 8px;
|
| 195 |
+
margin-bottom: 8px;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.member-name {
|
| 199 |
+
font-weight: 600;
|
| 200 |
+
color: var(--text-primary);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.member-stats {
|
| 204 |
+
display: flex;
|
| 205 |
+
gap: 12px;
|
| 206 |
+
font-size: 12px;
|
| 207 |
+
color: var(--text-muted);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.status-indicator {
|
| 211 |
+
width: 10px;
|
| 212 |
+
height: 10px;
|
| 213 |
+
border-radius: 50%;
|
| 214 |
+
display: inline-block;
|
| 215 |
+
margin-right: 8px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.status-online {
|
| 219 |
+
background: var(--success);
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.status-warning {
|
| 223 |
+
background: var(--warning);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.status-offline {
|
| 227 |
+
background: var(--danger);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.rotation-history {
|
| 231 |
+
background: var(--bg-card);
|
| 232 |
+
border-radius: var(--radius);
|
| 233 |
+
padding: 20px;
|
| 234 |
+
box-shadow: var(--shadow);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.history-item {
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
padding: 12px;
|
| 241 |
+
border-left: 3px solid var(--accent-primary);
|
| 242 |
+
background: var(--bg-hover);
|
| 243 |
+
border-radius: 4px;
|
| 244 |
+
margin-bottom: 12px;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.history-time {
|
| 248 |
+
font-size: 12px;
|
| 249 |
+
color: var(--text-muted);
|
| 250 |
+
margin-bottom: 4px;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.history-desc {
|
| 254 |
+
font-size: 14px;
|
| 255 |
+
color: var(--text-primary);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.modal {
|
| 259 |
+
display: none;
|
| 260 |
+
position: fixed;
|
| 261 |
+
top: 0;
|
| 262 |
+
left: 0;
|
| 263 |
+
width: 100%;
|
| 264 |
+
height: 100%;
|
| 265 |
+
background: rgba(0, 0, 0, 0.5);
|
| 266 |
+
z-index: 1000;
|
| 267 |
+
align-items: center;
|
| 268 |
+
justify-content: center;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.modal.active {
|
| 272 |
+
display: flex;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.modal-content {
|
| 276 |
+
background: var(--bg-secondary);
|
| 277 |
+
border-radius: var(--radius);
|
| 278 |
+
padding: 32px;
|
| 279 |
+
max-width: 600px;
|
| 280 |
+
width: 90%;
|
| 281 |
+
max-height: 90vh;
|
| 282 |
+
overflow-y: auto;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.modal-header {
|
| 286 |
+
font-size: 24px;
|
| 287 |
+
font-weight: 700;
|
| 288 |
+
margin-bottom: 24px;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.form-group {
|
| 292 |
+
margin-bottom: 20px;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.form-label {
|
| 296 |
+
display: block;
|
| 297 |
+
font-weight: 600;
|
| 298 |
+
margin-bottom: 8px;
|
| 299 |
+
color: var(--text-primary);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.form-input, .form-select, .form-textarea {
|
| 303 |
+
width: 100%;
|
| 304 |
+
padding: 12px;
|
| 305 |
+
border: 2px solid var(--border);
|
| 306 |
+
border-radius: 8px;
|
| 307 |
+
font-family: inherit;
|
| 308 |
+
font-size: 14px;
|
| 309 |
+
transition: all 0.3s;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
| 313 |
+
outline: none;
|
| 314 |
+
border-color: var(--accent-primary);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.form-textarea {
|
| 318 |
+
resize: vertical;
|
| 319 |
+
min-height: 100px;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.alert {
|
| 323 |
+
padding: 12px 16px;
|
| 324 |
+
border-radius: 8px;
|
| 325 |
+
margin-bottom: 16px;
|
| 326 |
+
font-size: 14px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.alert-success {
|
| 330 |
+
background: var(--success-bg);
|
| 331 |
+
color: var(--success);
|
| 332 |
+
border-left: 4px solid var(--success);
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.alert-error {
|
| 336 |
+
background: var(--danger-bg);
|
| 337 |
+
color: var(--danger);
|
| 338 |
+
border-left: 4px solid var(--danger);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.badge {
|
| 342 |
+
display: inline-block;
|
| 343 |
+
padding: 4px 10px;
|
| 344 |
+
border-radius: 999px;
|
| 345 |
+
font-size: 12px;
|
| 346 |
+
font-weight: 600;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.badge-success {
|
| 350 |
+
background: var(--success-bg);
|
| 351 |
+
color: var(--success);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.badge-warning {
|
| 355 |
+
background: var(--warning-bg);
|
| 356 |
+
color: var(--warning);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.badge-danger {
|
| 360 |
+
background: var(--danger-bg);
|
| 361 |
+
color: var(--danger);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.rate-limit-bar {
|
| 365 |
+
height: 8px;
|
| 366 |
+
background: var(--bg-hover);
|
| 367 |
+
border-radius: 4px;
|
| 368 |
+
overflow: hidden;
|
| 369 |
+
margin-top: 4px;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.rate-limit-fill {
|
| 373 |
+
height: 100%;
|
| 374 |
+
background: linear-gradient(90deg, var(--success) 0%, var(--warning) 50%, var(--danger) 100%);
|
| 375 |
+
transition: width 0.3s;
|
| 376 |
+
}
|
| 377 |
+
</style>
|
| 378 |
+
</head>
|
| 379 |
+
<body>
|
| 380 |
+
<div class="container">
|
| 381 |
+
<div class="header">
|
| 382 |
+
<h1>🔄 Source Pool Management</h1>
|
| 383 |
+
<p>Intelligent API source rotation and failover management</p>
|
| 384 |
+
<div class="header-actions">
|
| 385 |
+
<button class="btn btn-primary" onclick="showCreatePoolModal()">➕ Create New Pool</button>
|
| 386 |
+
<button class="btn btn-secondary" onclick="loadPools()">🔄 Refresh</button>
|
| 387 |
+
<button class="btn btn-secondary" onclick="window.location.href='index.html'">← Back to Dashboard</button>
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
|
| 391 |
+
<div id="alertContainer"></div>
|
| 392 |
+
|
| 393 |
+
<div id="poolsContainer" class="pools-grid">
|
| 394 |
+
<!-- Pools will be loaded here -->
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
<div class="rotation-history" style="margin-top: 24px;">
|
| 398 |
+
<h2 style="margin-bottom: 16px;">Recent Rotation Events</h2>
|
| 399 |
+
<div id="historyContainer">
|
| 400 |
+
<!-- History will be loaded here -->
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
|
| 405 |
+
<!-- Create Pool Modal -->
|
| 406 |
+
<div id="createPoolModal" class="modal">
|
| 407 |
+
<div class="modal-content">
|
| 408 |
+
<h2 class="modal-header">Create New Source Pool</h2>
|
| 409 |
+
<form id="createPoolForm">
|
| 410 |
+
<div class="form-group">
|
| 411 |
+
<label class="form-label">Pool Name</label>
|
| 412 |
+
<input type="text" class="form-input" id="poolName" required>
|
| 413 |
+
</div>
|
| 414 |
+
<div class="form-group">
|
| 415 |
+
<label class="form-label">Category</label>
|
| 416 |
+
<select class="form-select" id="poolCategory" required>
|
| 417 |
+
<option value="market_data">Market Data</option>
|
| 418 |
+
<option value="blockchain_explorers">Blockchain Explorers</option>
|
| 419 |
+
<option value="news">News</option>
|
| 420 |
+
<option value="sentiment">Sentiment</option>
|
| 421 |
+
<option value="onchain_analytics">On-Chain Analytics</option>
|
| 422 |
+
<option value="rpc_nodes">RPC Nodes</option>
|
| 423 |
+
</select>
|
| 424 |
+
</div>
|
| 425 |
+
<div class="form-group">
|
| 426 |
+
<label class="form-label">Rotation Strategy</label>
|
| 427 |
+
<select class="form-select" id="rotationStrategy" required>
|
| 428 |
+
<option value="round_robin">Round Robin</option>
|
| 429 |
+
<option value="least_used">Least Used</option>
|
| 430 |
+
<option value="priority">Priority Based</option>
|
| 431 |
+
<option value="weighted">Weighted</option>
|
| 432 |
+
</select>
|
| 433 |
+
</div>
|
| 434 |
+
<div class="form-group">
|
| 435 |
+
<label class="form-label">Description</label>
|
| 436 |
+
<textarea class="form-textarea" id="poolDescription"></textarea>
|
| 437 |
+
</div>
|
| 438 |
+
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
| 439 |
+
<button type="button" class="btn btn-secondary" onclick="closeCreatePoolModal()">Cancel</button>
|
| 440 |
+
<button type="submit" class="btn btn-primary">Create Pool</button>
|
| 441 |
+
</div>
|
| 442 |
+
</form>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<!-- Add Member Modal -->
|
| 447 |
+
<div id="addMemberModal" class="modal">
|
| 448 |
+
<div class="modal-content">
|
| 449 |
+
<h2 class="modal-header">Add Provider to Pool</h2>
|
| 450 |
+
<form id="addMemberForm">
|
| 451 |
+
<div class="form-group">
|
| 452 |
+
<label class="form-label">Provider</label>
|
| 453 |
+
<select class="form-select" id="memberProvider" required>
|
| 454 |
+
<!-- Will be populated dynamically -->
|
| 455 |
+
</select>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="form-group">
|
| 458 |
+
<label class="form-label">Priority (higher = better)</label>
|
| 459 |
+
<input type="number" class="form-input" id="memberPriority" value="1" min="1" max="10">
|
| 460 |
+
</div>
|
| 461 |
+
<div class="form-group">
|
| 462 |
+
<label class="form-label">Weight</label>
|
| 463 |
+
<input type="number" class="form-input" id="memberWeight" value="1" min="1" max="100">
|
| 464 |
+
</div>
|
| 465 |
+
<div style="display: flex; gap: 12px; justify-content: flex-end;">
|
| 466 |
+
<button type="button" class="btn btn-secondary" onclick="closeAddMemberModal()">Cancel</button>
|
| 467 |
+
<button type="submit" class="btn btn-primary">Add Member</button>
|
| 468 |
+
</div>
|
| 469 |
+
</form>
|
| 470 |
+
</div>
|
| 471 |
+
</div>
|
| 472 |
+
|
| 473 |
+
<script>
|
| 474 |
+
const API_BASE = window.location.origin;
|
| 475 |
+
let currentPoolId = null;
|
| 476 |
+
let allProviders = [];
|
| 477 |
+
|
| 478 |
+
// Load pools on page load
|
| 479 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 480 |
+
loadPools();
|
| 481 |
+
loadProviders();
|
| 482 |
+
});
|
| 483 |
+
|
| 484 |
+
async function loadPools() {
|
| 485 |
+
try {
|
| 486 |
+
const response = await fetch(`${API_BASE}/api/pools`);
|
| 487 |
+
const data = await response.json();
|
| 488 |
+
|
| 489 |
+
const container = document.getElementById('poolsContainer');
|
| 490 |
+
container.innerHTML = '';
|
| 491 |
+
|
| 492 |
+
if (data.pools.length === 0) {
|
| 493 |
+
container.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">No pools configured. Create your first pool to get started.</p>';
|
| 494 |
+
return;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
data.pools.forEach(pool => {
|
| 498 |
+
container.appendChild(createPoolCard(pool));
|
| 499 |
+
});
|
| 500 |
+
} catch (error) {
|
| 501 |
+
console.error('Error loading pools:', error);
|
| 502 |
+
showAlert('Failed to load pools', 'error');
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
function createPoolCard(pool) {
|
| 507 |
+
const card = document.createElement('div');
|
| 508 |
+
card.className = 'pool-card';
|
| 509 |
+
|
| 510 |
+
const currentProvider = pool.current_provider
|
| 511 |
+
? `<div style="margin-bottom: 12px;">
|
| 512 |
+
<span class="status-indicator status-online"></span>
|
| 513 |
+
Current: <strong>${pool.current_provider.name}</strong>
|
| 514 |
+
</div>`
|
| 515 |
+
: '<div style="margin-bottom: 12px; color: var(--text-muted);">No active provider</div>';
|
| 516 |
+
|
| 517 |
+
const membersHTML = pool.members.map(member => {
|
| 518 |
+
const successRate = member.success_rate || 0;
|
| 519 |
+
const statusClass = successRate >= 90 ? 'status-online' : successRate >= 70 ? 'status-warning' : 'status-offline';
|
| 520 |
+
|
| 521 |
+
let rateLimitHTML = '';
|
| 522 |
+
if (member.rate_limit) {
|
| 523 |
+
const percentage = member.rate_limit.percentage;
|
| 524 |
+
rateLimitHTML = `
|
| 525 |
+
<div style="margin-top: 4px;">
|
| 526 |
+
<div style="font-size: 11px; color: var(--text-muted);">
|
| 527 |
+
${member.rate_limit.usage}/${member.rate_limit.limit} (${percentage}%)
|
| 528 |
+
</div>
|
| 529 |
+
<div class="rate-limit-bar">
|
| 530 |
+
<div class="rate-limit-fill" style="width: ${percentage}%"></div>
|
| 531 |
+
</div>
|
| 532 |
+
</div>
|
| 533 |
+
`;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
return `
|
| 537 |
+
<div class="member-item">
|
| 538 |
+
<div>
|
| 539 |
+
<span class="status-indicator ${statusClass}"></span>
|
| 540 |
+
<span class="member-name">${member.provider_name}</span>
|
| 541 |
+
<div class="member-stats">
|
| 542 |
+
<span>Used: ${member.use_count}</span>
|
| 543 |
+
<span>Success: ${successRate.toFixed(1)}%</span>
|
| 544 |
+
<span>Priority: ${member.priority}</span>
|
| 545 |
+
</div>
|
| 546 |
+
${rateLimitHTML}
|
| 547 |
+
</div>
|
| 548 |
+
</div>
|
| 549 |
+
`;
|
| 550 |
+
}).join('');
|
| 551 |
+
|
| 552 |
+
card.innerHTML = `
|
| 553 |
+
<div class="pool-header">
|
| 554 |
+
<div>
|
| 555 |
+
<div class="pool-title">${pool.pool_name}</div>
|
| 556 |
+
<span class="pool-category">${pool.category}</span>
|
| 557 |
+
</div>
|
| 558 |
+
<div style="display: flex; gap: 8px;">
|
| 559 |
+
<button class="btn btn-sm btn-secondary" onclick="addMember(${pool.pool_id})">➕</button>
|
| 560 |
+
<button class="btn btn-sm btn-primary" onclick="rotatePool(${pool.pool_id})">🔄</button>
|
| 561 |
+
<button class="btn btn-sm btn-danger" onclick="deletePool(${pool.pool_id}, '${pool.pool_name}')">🗑️</button>
|
| 562 |
+
</div>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
${currentProvider}
|
| 566 |
+
|
| 567 |
+
<div class="pool-stats">
|
| 568 |
+
<div class="stat-item">
|
| 569 |
+
<div class="stat-label">Strategy</div>
|
| 570 |
+
<div class="stat-value" style="font-size: 14px;">${pool.rotation_strategy}</div>
|
| 571 |
+
</div>
|
| 572 |
+
<div class="stat-item">
|
| 573 |
+
<div class="stat-label">Total Rotations</div>
|
| 574 |
+
<div class="stat-value">${pool.total_rotations}</div>
|
| 575 |
+
</div>
|
| 576 |
+
<div class="stat-item">
|
| 577 |
+
<div class="stat-label">Members</div>
|
| 578 |
+
<div class="stat-value">${pool.members.length}</div>
|
| 579 |
+
</div>
|
| 580 |
+
<div class="stat-item">
|
| 581 |
+
<div class="stat-label">Status</div>
|
| 582 |
+
<div class="stat-value">
|
| 583 |
+
<span class="badge ${pool.enabled ? 'badge-success' : 'badge-danger'}">
|
| 584 |
+
${pool.enabled ? 'Enabled' : 'Disabled'}
|
| 585 |
+
</span>
|
| 586 |
+
</div>
|
| 587 |
+
</div>
|
| 588 |
+
</div>
|
| 589 |
+
|
| 590 |
+
<div class="pool-members">
|
| 591 |
+
<div style="font-weight: 600; margin-bottom: 12px;">Pool Members</div>
|
| 592 |
+
${membersHTML || '<div style="color: var(--text-muted); font-size: 14px;">No members</div>'}
|
| 593 |
+
</div>
|
| 594 |
+
`;
|
| 595 |
+
|
| 596 |
+
return card;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
async function loadProviders() {
|
| 600 |
+
try {
|
| 601 |
+
const response = await fetch(`${API_BASE}/api/providers`);
|
| 602 |
+
const providers = await response.json();
|
| 603 |
+
allProviders = providers;
|
| 604 |
+
|
| 605 |
+
const select = document.getElementById('memberProvider');
|
| 606 |
+
select.innerHTML = providers.map(p =>
|
| 607 |
+
`<option value="${p.id}">${p.name} (${p.category})</option>`
|
| 608 |
+
).join('');
|
| 609 |
+
} catch (error) {
|
| 610 |
+
console.error('Error loading providers:', error);
|
| 611 |
+
}
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
function showCreatePoolModal() {
|
| 615 |
+
document.getElementById('createPoolModal').classList.add('active');
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
function closeCreatePoolModal() {
|
| 619 |
+
document.getElementById('createPoolModal').classList.remove('active');
|
| 620 |
+
document.getElementById('createPoolForm').reset();
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
function showAddMemberModal() {
|
| 624 |
+
document.getElementById('addMemberModal').classList.add('active');
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
function closeAddMemberModal() {
|
| 628 |
+
document.getElementById('addMemberModal').classList.remove('active');
|
| 629 |
+
document.getElementById('addMemberForm').reset();
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
function addMember(poolId) {
|
| 633 |
+
currentPoolId = poolId;
|
| 634 |
+
showAddMemberModal();
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
document.getElementById('createPoolForm').addEventListener('submit', async (e) => {
|
| 638 |
+
e.preventDefault();
|
| 639 |
+
|
| 640 |
+
const data = {
|
| 641 |
+
name: document.getElementById('poolName').value,
|
| 642 |
+
category: document.getElementById('poolCategory').value,
|
| 643 |
+
rotation_strategy: document.getElementById('rotationStrategy').value,
|
| 644 |
+
description: document.getElementById('poolDescription').value
|
| 645 |
+
};
|
| 646 |
+
|
| 647 |
+
try {
|
| 648 |
+
const response = await fetch(`${API_BASE}/api/pools`, {
|
| 649 |
+
method: 'POST',
|
| 650 |
+
headers: { 'Content-Type': 'application/json' },
|
| 651 |
+
body: JSON.stringify(data)
|
| 652 |
+
});
|
| 653 |
+
|
| 654 |
+
if (response.ok) {
|
| 655 |
+
showAlert('Pool created successfully', 'success');
|
| 656 |
+
closeCreatePoolModal();
|
| 657 |
+
loadPools();
|
| 658 |
+
} else {
|
| 659 |
+
const error = await response.json();
|
| 660 |
+
showAlert(error.detail || 'Failed to create pool', 'error');
|
| 661 |
+
}
|
| 662 |
+
} catch (error) {
|
| 663 |
+
console.error('Error creating pool:', error);
|
| 664 |
+
showAlert('Failed to create pool', 'error');
|
| 665 |
+
}
|
| 666 |
+
});
|
| 667 |
+
|
| 668 |
+
document.getElementById('addMemberForm').addEventListener('submit', async (e) => {
|
| 669 |
+
e.preventDefault();
|
| 670 |
+
|
| 671 |
+
const data = {
|
| 672 |
+
provider_id: parseInt(document.getElementById('memberProvider').value),
|
| 673 |
+
priority: parseInt(document.getElementById('memberPriority').value),
|
| 674 |
+
weight: parseInt(document.getElementById('memberWeight').value)
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
try {
|
| 678 |
+
const response = await fetch(`${API_BASE}/api/pools/${currentPoolId}/members`, {
|
| 679 |
+
method: 'POST',
|
| 680 |
+
headers: { 'Content-Type': 'application/json' },
|
| 681 |
+
body: JSON.stringify(data)
|
| 682 |
+
});
|
| 683 |
+
|
| 684 |
+
if (response.ok) {
|
| 685 |
+
showAlert('Member added successfully', 'success');
|
| 686 |
+
closeAddMemberModal();
|
| 687 |
+
loadPools();
|
| 688 |
+
} else {
|
| 689 |
+
const error = await response.json();
|
| 690 |
+
showAlert(error.detail || 'Failed to add member', 'error');
|
| 691 |
+
}
|
| 692 |
+
} catch (error) {
|
| 693 |
+
console.error('Error adding member:', error);
|
| 694 |
+
showAlert('Failed to add member', 'error');
|
| 695 |
+
}
|
| 696 |
+
});
|
| 697 |
+
|
| 698 |
+
async function rotatePool(poolId) {
|
| 699 |
+
try {
|
| 700 |
+
const response = await fetch(`${API_BASE}/api/pools/${poolId}/rotate`, {
|
| 701 |
+
method: 'POST',
|
| 702 |
+
headers: { 'Content-Type': 'application/json' },
|
| 703 |
+
body: JSON.stringify({ reason: 'manual' })
|
| 704 |
+
});
|
| 705 |
+
|
| 706 |
+
if (response.ok) {
|
| 707 |
+
const result = await response.json();
|
| 708 |
+
showAlert(`Rotated to ${result.provider_name}`, 'success');
|
| 709 |
+
loadPools();
|
| 710 |
+
} else {
|
| 711 |
+
const error = await response.json();
|
| 712 |
+
showAlert(error.detail || 'Failed to rotate', 'error');
|
| 713 |
+
}
|
| 714 |
+
} catch (error) {
|
| 715 |
+
console.error('Error rotating pool:', error);
|
| 716 |
+
showAlert('Failed to rotate pool', 'error');
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
async function deletePool(poolId, poolName) {
|
| 721 |
+
if (!confirm(`Are you sure you want to delete pool "${poolName}"?`)) {
|
| 722 |
+
return;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
try {
|
| 726 |
+
const response = await fetch(`${API_BASE}/api/pools/${poolId}`, {
|
| 727 |
+
method: 'DELETE'
|
| 728 |
+
});
|
| 729 |
+
|
| 730 |
+
if (response.ok) {
|
| 731 |
+
showAlert('Pool deleted successfully', 'success');
|
| 732 |
+
loadPools();
|
| 733 |
+
} else {
|
| 734 |
+
const error = await response.json();
|
| 735 |
+
showAlert(error.detail || 'Failed to delete pool', 'error');
|
| 736 |
+
}
|
| 737 |
+
} catch (error) {
|
| 738 |
+
console.error('Error deleting pool:', error);
|
| 739 |
+
showAlert('Failed to delete pool', 'error');
|
| 740 |
+
}
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
function showAlert(message, type) {
|
| 744 |
+
const container = document.getElementById('alertContainer');
|
| 745 |
+
const alert = document.createElement('div');
|
| 746 |
+
alert.className = `alert alert-${type}`;
|
| 747 |
+
alert.textContent = message;
|
| 748 |
+
container.appendChild(alert);
|
| 749 |
+
|
| 750 |
+
setTimeout(() => {
|
| 751 |
+
alert.remove();
|
| 752 |
+
}, 5000);
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// Close modals when clicking outside
|
| 756 |
+
document.querySelectorAll('.modal').forEach(modal => {
|
| 757 |
+
modal.addEventListener('click', (e) => {
|
| 758 |
+
if (e.target === modal) {
|
| 759 |
+
modal.classList.remove('active');
|
| 760 |
+
}
|
| 761 |
+
});
|
| 762 |
+
});
|
| 763 |
+
</script>
|
| 764 |
+
</body>
|
| 765 |
+
</html>
|
archive_html/simple_overview.html
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto Monitor - Complete Overview</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
padding: 20px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 1400px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.header {
|
| 27 |
+
background: white;
|
| 28 |
+
border-radius: 15px;
|
| 29 |
+
padding: 30px;
|
| 30 |
+
margin-bottom: 20px;
|
| 31 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
| 32 |
+
display: flex;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: center;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.header h1 {
|
| 38 |
+
color: #667eea;
|
| 39 |
+
font-size: 2em;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.refresh-btn {
|
| 43 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 44 |
+
color: white;
|
| 45 |
+
border: none;
|
| 46 |
+
padding: 12px 30px;
|
| 47 |
+
border-radius: 10px;
|
| 48 |
+
font-size: 1em;
|
| 49 |
+
font-weight: 600;
|
| 50 |
+
cursor: pointer;
|
| 51 |
+
transition: all 0.3s ease;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.refresh-btn:hover {
|
| 55 |
+
transform: translateY(-2px);
|
| 56 |
+
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.stats-grid {
|
| 60 |
+
display: grid;
|
| 61 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 62 |
+
gap: 15px;
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.stat-card {
|
| 67 |
+
background: white;
|
| 68 |
+
border-radius: 12px;
|
| 69 |
+
padding: 20px;
|
| 70 |
+
text-align: center;
|
| 71 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.stat-card h3 {
|
| 75 |
+
color: #999;
|
| 76 |
+
font-size: 0.85em;
|
| 77 |
+
text-transform: uppercase;
|
| 78 |
+
margin-bottom: 10px;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.stat-card .value {
|
| 82 |
+
font-size: 2.5em;
|
| 83 |
+
font-weight: bold;
|
| 84 |
+
margin-bottom: 5px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.stat-card.green .value { color: #10b981; }
|
| 88 |
+
.stat-card.blue .value { color: #3b82f6; }
|
| 89 |
+
.stat-card.orange .value { color: #f59e0b; }
|
| 90 |
+
.stat-card.red .value { color: #ef4444; }
|
| 91 |
+
|
| 92 |
+
.content-grid {
|
| 93 |
+
display: grid;
|
| 94 |
+
grid-template-columns: 2fr 1fr;
|
| 95 |
+
gap: 20px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.card {
|
| 99 |
+
background: white;
|
| 100 |
+
border-radius: 15px;
|
| 101 |
+
padding: 25px;
|
| 102 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.card h2 {
|
| 106 |
+
color: #333;
|
| 107 |
+
margin-bottom: 20px;
|
| 108 |
+
padding-bottom: 10px;
|
| 109 |
+
border-bottom: 3px solid #667eea;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.providers-list {
|
| 113 |
+
display: grid;
|
| 114 |
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
| 115 |
+
gap: 10px;
|
| 116 |
+
max-height: 600px;
|
| 117 |
+
overflow-y: auto;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.provider-item {
|
| 121 |
+
background: #f8f9fa;
|
| 122 |
+
border-radius: 8px;
|
| 123 |
+
padding: 12px;
|
| 124 |
+
border-left: 4px solid #ddd;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.provider-item.online {
|
| 128 |
+
border-left-color: #10b981;
|
| 129 |
+
background: linear-gradient(to right, #f0fdf4, #f8f9fa);
|
| 130 |
+
}
|
| 131 |
+
.provider-item.offline {
|
| 132 |
+
border-left-color: #ef4444;
|
| 133 |
+
background: linear-gradient(to right, #fef2f2, #f8f9fa);
|
| 134 |
+
}
|
| 135 |
+
.provider-item.degraded {
|
| 136 |
+
border-left-color: #f59e0b;
|
| 137 |
+
background: linear-gradient(to right, #fffbeb, #f8f9fa);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.provider-item .name {
|
| 141 |
+
font-weight: 600;
|
| 142 |
+
color: #333;
|
| 143 |
+
font-size: 0.9em;
|
| 144 |
+
margin-bottom: 5px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.provider-item .info {
|
| 148 |
+
font-size: 0.75em;
|
| 149 |
+
color: #666;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.category-list {
|
| 153 |
+
display: flex;
|
| 154 |
+
flex-direction: column;
|
| 155 |
+
gap: 12px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.category-item {
|
| 159 |
+
background: #f8f9fa;
|
| 160 |
+
border-radius: 8px;
|
| 161 |
+
padding: 15px;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.category-item .name {
|
| 165 |
+
font-weight: 600;
|
| 166 |
+
color: #333;
|
| 167 |
+
margin-bottom: 8px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.category-item .stats {
|
| 171 |
+
display: flex;
|
| 172 |
+
gap: 10px;
|
| 173 |
+
font-size: 0.85em;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.loading {
|
| 177 |
+
text-align: center;
|
| 178 |
+
padding: 40px;
|
| 179 |
+
color: #666;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
@media (max-width: 768px) {
|
| 183 |
+
.content-grid {
|
| 184 |
+
grid-template-columns: 1fr;
|
| 185 |
+
}
|
| 186 |
+
.stats-grid {
|
| 187 |
+
grid-template-columns: repeat(2, 1fr);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
</style>
|
| 191 |
+
</head>
|
| 192 |
+
<body>
|
| 193 |
+
<div class="container">
|
| 194 |
+
<div class="header">
|
| 195 |
+
<div>
|
| 196 |
+
<h1>🚀 Crypto API Monitor</h1>
|
| 197 |
+
<p style="color: #666; margin-top: 5px;">Complete System Overview</p>
|
| 198 |
+
</div>
|
| 199 |
+
<button class="refresh-btn" onclick="loadData()">🔄 Refresh</button>
|
| 200 |
+
</div>
|
| 201 |
+
|
| 202 |
+
<div class="stats-grid">
|
| 203 |
+
<div class="stat-card blue">
|
| 204 |
+
<h3>Total APIs</h3>
|
| 205 |
+
<div class="value" id="total">-</div>
|
| 206 |
+
</div>
|
| 207 |
+
<div class="stat-card green">
|
| 208 |
+
<h3>Online</h3>
|
| 209 |
+
<div class="value" id="online">-</div>
|
| 210 |
+
</div>
|
| 211 |
+
<div class="stat-card orange">
|
| 212 |
+
<h3>Degraded</h3>
|
| 213 |
+
<div class="value" id="degraded">-</div>
|
| 214 |
+
</div>
|
| 215 |
+
<div class="stat-card red">
|
| 216 |
+
<h3>Offline</h3>
|
| 217 |
+
<div class="value" id="offline">-</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<div class="content-grid">
|
| 222 |
+
<div class="card">
|
| 223 |
+
<h2>📊 All Providers</h2>
|
| 224 |
+
<div class="providers-list" id="providers">
|
| 225 |
+
<div class="loading">Loading...</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<div class="card">
|
| 230 |
+
<h2>📁 Categories</h2>
|
| 231 |
+
<div class="category-list" id="categories">
|
| 232 |
+
<div class="loading">Loading...</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<script>
|
| 239 |
+
async function loadData() {
|
| 240 |
+
try {
|
| 241 |
+
const response = await fetch('/api/providers');
|
| 242 |
+
const providers = await response.json();
|
| 243 |
+
|
| 244 |
+
// Calculate stats
|
| 245 |
+
const online = providers.filter(p => p.status === 'online').length;
|
| 246 |
+
const offline = providers.filter(p => p.status === 'offline').length;
|
| 247 |
+
const degraded = providers.filter(p => p.status === 'degraded').length;
|
| 248 |
+
|
| 249 |
+
// Update stats
|
| 250 |
+
document.getElementById('total').textContent = providers.length;
|
| 251 |
+
document.getElementById('online').textContent = online;
|
| 252 |
+
document.getElementById('degraded').textContent = degraded;
|
| 253 |
+
document.getElementById('offline').textContent = offline;
|
| 254 |
+
|
| 255 |
+
// Group by category
|
| 256 |
+
const categories = {};
|
| 257 |
+
providers.forEach(p => {
|
| 258 |
+
if (!categories[p.category]) {
|
| 259 |
+
categories[p.category] = { online: 0, offline: 0, degraded: 0 };
|
| 260 |
+
}
|
| 261 |
+
categories[p.category][p.status]++;
|
| 262 |
+
});
|
| 263 |
+
|
| 264 |
+
// Display providers
|
| 265 |
+
const providersHtml = providers.map(p => `
|
| 266 |
+
<div class="provider-item ${p.status}">
|
| 267 |
+
<div class="name">${p.name}</div>
|
| 268 |
+
<div class="info">${p.category}</div>
|
| 269 |
+
<div class="info" style="color: ${p.status === 'online' ? '#10b981' : p.status === 'degraded' ? '#f59e0b' : '#ef4444'}">
|
| 270 |
+
${p.status.toUpperCase()}
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
`).join('');
|
| 274 |
+
document.getElementById('providers').innerHTML = providersHtml;
|
| 275 |
+
|
| 276 |
+
// Display categories
|
| 277 |
+
const categoriesHtml = Object.entries(categories).map(([name, stats]) => `
|
| 278 |
+
<div class="category-item">
|
| 279 |
+
<div class="name">${name}</div>
|
| 280 |
+
<div class="stats">
|
| 281 |
+
<span style="color: #10b981;">✓ ${stats.online}</span>
|
| 282 |
+
<span style="color: #f59e0b;">⚠ ${stats.degraded}</span>
|
| 283 |
+
<span style="color: #ef4444;">✗ ${stats.offline}</span>
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
`).join('');
|
| 287 |
+
document.getElementById('categories').innerHTML = categoriesHtml;
|
| 288 |
+
|
| 289 |
+
} catch (error) {
|
| 290 |
+
console.error('Error:', error);
|
| 291 |
+
document.getElementById('providers').innerHTML = '<div class="loading">Error loading data</div>';
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
// Load on start
|
| 296 |
+
loadData();
|
| 297 |
+
|
| 298 |
+
// Auto-refresh every 30 seconds
|
| 299 |
+
setInterval(loadData, 30000);
|
| 300 |
+
</script>
|
| 301 |
+
</body>
|
| 302 |
+
</html>
|
| 303 |
+
|
archive_html/unified_dashboard.html
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>Crypto Monitor HF - Unified Dashboard</title>
|
| 7 |
+
<link rel="stylesheet" href="static/css/design-tokens.css" />
|
| 8 |
+
<link rel="stylesheet" href="static/css/design-system.css" />
|
| 9 |
+
<link rel="stylesheet" href="static/css/dashboard.css" />
|
| 10 |
+
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
|
| 11 |
+
<link rel="stylesheet" href="static/css/modern-dashboard.css" />
|
| 12 |
+
<link rel="stylesheet" href="static/css/glassmorphism.css" />
|
| 13 |
+
<link rel="stylesheet" href="static/css/light-minimal-theme.css" />
|
| 14 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script>
|
| 15 |
+
<script src="static/js/animations.js" defer></script>
|
| 16 |
+
<script src="static/js/menu-system.js" defer></script>
|
| 17 |
+
<script src="static/js/huggingface-integration.js" defer></script>
|
| 18 |
+
</head>
|
| 19 |
+
<body data-theme="light">
|
| 20 |
+
<div class="app-shell">
|
| 21 |
+
<aside class="sidebar sidebar-modern">
|
| 22 |
+
<div class="brand brand-modern">
|
| 23 |
+
<div class="brand-icon">
|
| 24 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 25 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
|
| 26 |
+
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" />
|
| 27 |
+
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="1.5" />
|
| 28 |
+
</svg>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="brand-text">
|
| 31 |
+
<strong>Crypto Monitor HF</strong>
|
| 32 |
+
<span class="env-pill">
|
| 33 |
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 34 |
+
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" />
|
| 35 |
+
</svg>
|
| 36 |
+
HF Space
|
| 37 |
+
</span>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
<nav class="nav nav-modern">
|
| 41 |
+
<button class="nav-button nav-button-modern active" data-nav="page-overview">
|
| 42 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 43 |
+
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 44 |
+
<path d="M9 22V12h6v10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 45 |
+
</svg>
|
| 46 |
+
<span>Overview</span>
|
| 47 |
+
</button>
|
| 48 |
+
<button class="nav-button nav-button-modern" data-nav="page-market">
|
| 49 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 50 |
+
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 51 |
+
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 52 |
+
</svg>
|
| 53 |
+
<span>Market</span>
|
| 54 |
+
</button>
|
| 55 |
+
<button class="nav-button nav-button-modern" data-nav="page-chart">
|
| 56 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 57 |
+
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 58 |
+
<path d="M7 16l4-4 4 4 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 59 |
+
</svg>
|
| 60 |
+
<span>Chart Lab</span>
|
| 61 |
+
</button>
|
| 62 |
+
<button class="nav-button nav-button-modern" data-nav="page-ai">
|
| 63 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 64 |
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 65 |
+
</svg>
|
| 66 |
+
<span>Sentiment & AI</span>
|
| 67 |
+
</button>
|
| 68 |
+
<button class="nav-button nav-button-modern" data-nav="page-news">
|
| 69 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 70 |
+
<path d="M4 19.5A2.5 2.5 0 016.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 71 |
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 72 |
+
</svg>
|
| 73 |
+
<span>News</span>
|
| 74 |
+
</button>
|
| 75 |
+
<button class="nav-button nav-button-modern" data-nav="page-providers">
|
| 76 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 77 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 78 |
+
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 79 |
+
</svg>
|
| 80 |
+
<span>Providers</span>
|
| 81 |
+
</button>
|
| 82 |
+
<button class="nav-button nav-button-modern" data-nav="page-api">
|
| 83 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 84 |
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 85 |
+
</svg>
|
| 86 |
+
<span>API Explorer</span>
|
| 87 |
+
</button>
|
| 88 |
+
<button class="nav-button nav-button-modern" data-nav="page-debug">
|
| 89 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 90 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
| 91 |
+
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 92 |
+
</svg>
|
| 93 |
+
<span>Diagnostics</span>
|
| 94 |
+
</button>
|
| 95 |
+
<button class="nav-button nav-button-modern" data-nav="page-datasets">
|
| 96 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 97 |
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
| 98 |
+
</svg>
|
| 99 |
+
<span>Datasets & Models</span>
|
| 100 |
+
</button>
|
| 101 |
+
<button class="nav-button nav-button-modern" data-nav="page-settings">
|
| 102 |
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 103 |
+
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
| 104 |
+
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 105 |
+
</svg>
|
| 106 |
+
<span>Settings</span>
|
| 107 |
+
</button>
|
| 108 |
+
</nav>
|
| 109 |
+
<div class="sidebar-footer">
|
| 110 |
+
<div class="footer-badge">
|
| 111 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 112 |
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
| 113 |
+
</svg>
|
| 114 |
+
Unified Intelligence Console
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</aside>
|
| 118 |
+
<main class="main-area">
|
| 119 |
+
<header class="modern-header">
|
| 120 |
+
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%; flex-wrap: wrap; gap: 16px;">
|
| 121 |
+
<div>
|
| 122 |
+
<h1 style="margin: 0; font-size: 1.75rem; font-weight: 800; background: linear-gradient(135deg, #00D4FF, #8B5CF6, #EC4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">Unified Intelligence Dashboard</h1>
|
| 123 |
+
<p class="text-muted" style="margin: 4px 0 0 0; font-size: 0.875rem;">Live market telemetry, AI signals, diagnostics, and provider health.</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="status-group" style="display: flex; gap: 12px; align-items: center; position: relative;">
|
| 126 |
+
<div class="status-pill" data-api-health data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
|
| 127 |
+
<span class="status-dot"></span>
|
| 128 |
+
<span>checking</span>
|
| 129 |
+
</div>
|
| 130 |
+
<div class="status-pill" data-ws-status data-state="warn" style="padding: 8px 16px; border-radius: 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1);">
|
| 131 |
+
<span class="status-dot"></span>
|
| 132 |
+
<span>connecting</span>
|
| 133 |
+
</div>
|
| 134 |
+
<button class="button-3d" data-menu-trigger="theme-menu" style="padding: 8px 16px; position: relative;">
|
| 135 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
| 136 |
+
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
| 137 |
+
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
| 138 |
+
</svg>
|
| 139 |
+
</button>
|
| 140 |
+
<div class="menu-dropdown" data-menu="theme-menu" style="display: none; top: 100%; right: 0; margin-top: 8px;">
|
| 141 |
+
<div class="menu-item" data-action="theme-light">
|
| 142 |
+
<span>☀️ Light Theme</span>
|
| 143 |
+
</div>
|
| 144 |
+
<div class="menu-item" data-action="theme-dark">
|
| 145 |
+
<span>🌙 Dark Theme</span>
|
| 146 |
+
</div>
|
| 147 |
+
<div class="menu-separator"></div>
|
| 148 |
+
<div class="menu-item" data-action="settings">
|
| 149 |
+
<span>⚙️ Settings</span>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="header-crypto-list" data-header-crypto-list style="margin-top: 16px; width: 100%;">
|
| 155 |
+
<!-- Crypto list will be populated by JavaScript -->
|
| 156 |
+
</div>
|
| 157 |
+
</header>
|
| 158 |
+
<div class="page-container">
|
| 159 |
+
<section id="page-overview" class="page active">
|
| 160 |
+
<div class="section-header">
|
| 161 |
+
<h2 class="section-title">Global Overview</h2>
|
| 162 |
+
<span class="chip">Powered by /api/market/stats</span>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="stats-grid" data-overview-stats></div>
|
| 165 |
+
<div class="grid-two">
|
| 166 |
+
<div class="glass-card">
|
| 167 |
+
<div class="section-header">
|
| 168 |
+
<h3>Top Coins</h3>
|
| 169 |
+
<span class="text-muted">Market movers</span>
|
| 170 |
+
</div>
|
| 171 |
+
<div class="table-wrapper">
|
| 172 |
+
<table>
|
| 173 |
+
<thead>
|
| 174 |
+
<tr>
|
| 175 |
+
<th>#</th>
|
| 176 |
+
<th>Symbol</th>
|
| 177 |
+
<th>Name</th>
|
| 178 |
+
<th>Price</th>
|
| 179 |
+
<th>24h %</th>
|
| 180 |
+
<th>Volume</th>
|
| 181 |
+
<th>Market Cap</th>
|
| 182 |
+
</tr>
|
| 183 |
+
</thead>
|
| 184 |
+
<tbody data-top-coins-body></tbody>
|
| 185 |
+
</table>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
<div class="glass-card">
|
| 189 |
+
<div class="section-header">
|
| 190 |
+
<h3>Global Sentiment</h3>
|
| 191 |
+
<span class="text-muted">CryptoBERT stack</span>
|
| 192 |
+
</div>
|
| 193 |
+
<canvas id="sentiment-chart" height="220"></canvas>
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<div class="glass-card" style="margin-top: 24px;">
|
| 197 |
+
<div class="section-header">
|
| 198 |
+
<h3>Backend Information</h3>
|
| 199 |
+
<span class="text-muted">System Status</span>
|
| 200 |
+
</div>
|
| 201 |
+
<div data-backend-info class="backend-info-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 16px;">
|
| 202 |
+
<div class="backend-info-item">
|
| 203 |
+
<span class="info-label">API Status</span>
|
| 204 |
+
<span class="info-value" data-api-status>Checking...</span>
|
| 205 |
+
</div>
|
| 206 |
+
<div class="backend-info-item">
|
| 207 |
+
<span class="info-label">WebSocket</span>
|
| 208 |
+
<span class="info-value" data-ws-status>Connecting...</span>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="backend-info-item">
|
| 211 |
+
<span class="info-label">Providers</span>
|
| 212 |
+
<span class="info-value" data-providers-count>—</span>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="backend-info-item">
|
| 215 |
+
<span class="info-label">Last Update</span>
|
| 216 |
+
<span class="info-value" data-last-update>—</span>
|
| 217 |
+
</div>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</section>
|
| 221 |
+
|
| 222 |
+
<section id="page-market" class="page">
|
| 223 |
+
<div class="section-header">
|
| 224 |
+
<h2 class="section-title">Market Intelligence</h2>
|
| 225 |
+
<div class="controls-bar">
|
| 226 |
+
<div class="input-chip">
|
| 227 |
+
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M21 20l-5.6-5.6A6.5 6.5 0 1 0 15.4 16L21 21zM5 10.5a5.5 5.5 0 1 1 11 0a5.5 5.5 0 0 1-11 0z" fill="currentColor"/></svg>
|
| 228 |
+
<input type="text" placeholder="Search symbol" data-market-search />
|
| 229 |
+
</div>
|
| 230 |
+
<div class="input-chip">
|
| 231 |
+
Timeframe:
|
| 232 |
+
<button class="ghost" data-timeframe="1d">1D</button>
|
| 233 |
+
<button class="ghost active" data-timeframe="7d">7D</button>
|
| 234 |
+
<button class="ghost" data-timeframe="30d">30D</button>
|
| 235 |
+
</div>
|
| 236 |
+
<label class="input-chip"> Live updates
|
| 237 |
+
<div class="toggle">
|
| 238 |
+
<input type="checkbox" data-live-toggle />
|
| 239 |
+
<span></span>
|
| 240 |
+
</div>
|
| 241 |
+
</label>
|
| 242 |
+
</div>
|
| 243 |
+
</div>
|
| 244 |
+
<div class="glass-card">
|
| 245 |
+
<div class="table-wrapper">
|
| 246 |
+
<table>
|
| 247 |
+
<thead>
|
| 248 |
+
<tr>
|
| 249 |
+
<th>#</th>
|
| 250 |
+
<th>Symbol</th>
|
| 251 |
+
<th>Name</th>
|
| 252 |
+
<th>Price</th>
|
| 253 |
+
<th>24h %</th>
|
| 254 |
+
<th>Volume</th>
|
| 255 |
+
<th>Market Cap</th>
|
| 256 |
+
</tr>
|
| 257 |
+
</thead>
|
| 258 |
+
<tbody data-market-body></tbody>
|
| 259 |
+
</table>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
<div class="drawer" data-market-drawer>
|
| 263 |
+
<button class="ghost" data-close-drawer>Close</button>
|
| 264 |
+
<h3 data-drawer-symbol>—</h3>
|
| 265 |
+
<div data-drawer-stats></div>
|
| 266 |
+
<div class="glass-card" data-chart-wrapper>
|
| 267 |
+
<canvas id="market-detail-chart" height="180"></canvas>
|
| 268 |
+
</div>
|
| 269 |
+
<div class="glass-card">
|
| 270 |
+
<h4>Related Headlines</h4>
|
| 271 |
+
<div data-drawer-news></div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</section>
|
| 275 |
+
|
| 276 |
+
<section id="page-chart" class="page">
|
| 277 |
+
<div class="section-header">
|
| 278 |
+
<h2 class="section-title">Chart Lab - TradingView Style</h2>
|
| 279 |
+
<div class="controls-bar">
|
| 280 |
+
<select data-chart-symbol style="padding: 8px 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: var(--text-primary);">
|
| 281 |
+
<option value="BTC">BTC</option>
|
| 282 |
+
<option value="ETH">ETH</option>
|
| 283 |
+
<option value="SOL">SOL</option>
|
| 284 |
+
<option value="BNB">BNB</option>
|
| 285 |
+
<option value="ADA">ADA</option>
|
| 286 |
+
<option value="DOT">DOT</option>
|
| 287 |
+
<option value="MATIC">MATIC</option>
|
| 288 |
+
<option value="AVAX">AVAX</option>
|
| 289 |
+
</select>
|
| 290 |
+
<div class="chart-toolbar">
|
| 291 |
+
<button class="chart-timeframe-btn active" data-chart-timeframe="1d">1D</button>
|
| 292 |
+
<button class="chart-timeframe-btn" data-chart-timeframe="7d">7D</button>
|
| 293 |
+
<button class="chart-timeframe-btn" data-chart-timeframe="30d">30D</button>
|
| 294 |
+
<button class="chart-timeframe-btn" data-chart-timeframe="90d">90D</button>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="tradingview-chart-container glass-vibrant">
|
| 299 |
+
<div class="chart-toolbar">
|
| 300 |
+
<div class="chart-indicators">
|
| 301 |
+
<label class="chart-indicator-toggle">
|
| 302 |
+
<input type="checkbox" data-indicator="MA20" checked />
|
| 303 |
+
<span>MA 20</span>
|
| 304 |
+
</label>
|
| 305 |
+
<label class="chart-indicator-toggle">
|
| 306 |
+
<input type="checkbox" data-indicator="MA50" />
|
| 307 |
+
<span>MA 50</span>
|
| 308 |
+
</label>
|
| 309 |
+
<label class="chart-indicator-toggle">
|
| 310 |
+
<input type="checkbox" data-indicator="RSI" />
|
| 311 |
+
<span>RSI</span>
|
| 312 |
+
</label>
|
| 313 |
+
<label class="chart-indicator-toggle">
|
| 314 |
+
<input type="checkbox" data-indicator="Volume" checked />
|
| 315 |
+
<span>Volume</span>
|
| 316 |
+
</label>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
<canvas id="chart-lab-canvas" height="400"></canvas>
|
| 320 |
+
</div>
|
| 321 |
+
<div class="glass-card glass-vibrant" style="margin-top: 24px;">
|
| 322 |
+
<div class="controls-bar" style="margin-bottom: 16px;">
|
| 323 |
+
<button class="primary" data-run-analysis style="background: linear-gradient(135deg, #00D4FF, #8B5CF6);">Analyze Chart with AI</button>
|
| 324 |
+
</div>
|
| 325 |
+
<div data-ai-insights class="ai-insights"></div>
|
| 326 |
+
</div>
|
| 327 |
+
</section>
|
| 328 |
+
|
| 329 |
+
<section id="page-ai" class="page">
|
| 330 |
+
<div class="section-header">
|
| 331 |
+
<h2 class="section-title">Sentiment & AI Advisor</h2>
|
| 332 |
+
</div>
|
| 333 |
+
<div class="glass-card">
|
| 334 |
+
<form data-ai-form class="ai-form">
|
| 335 |
+
<div class="grid-two">
|
| 336 |
+
<label>Symbol
|
| 337 |
+
<select name="symbol">
|
| 338 |
+
<option value="BTC">BTC</option>
|
| 339 |
+
<option value="ETH">ETH</option>
|
| 340 |
+
<option value="SOL">SOL</option>
|
| 341 |
+
</select>
|
| 342 |
+
</label>
|
| 343 |
+
<label>Time Horizon
|
| 344 |
+
<select name="horizon">
|
| 345 |
+
<option value="intraday">Intraday</option>
|
| 346 |
+
<option value="swing" selected>Swing</option>
|
| 347 |
+
<option value="long">Long Term</option>
|
| 348 |
+
</select>
|
| 349 |
+
</label>
|
| 350 |
+
<label>Risk Profile
|
| 351 |
+
<select name="risk">
|
| 352 |
+
<option value="conservative">Conservative</option>
|
| 353 |
+
<option value="moderate" selected>Moderate</option>
|
| 354 |
+
<option value="aggressive">Aggressive</option>
|
| 355 |
+
</select>
|
| 356 |
+
</label>
|
| 357 |
+
<label>Sentiment Model
|
| 358 |
+
<select name="model">
|
| 359 |
+
<option value="auto">Auto</option>
|
| 360 |
+
<option value="crypto">CryptoBERT</option>
|
| 361 |
+
<option value="financial">FinBERT</option>
|
| 362 |
+
<option value="social">Twitter Sentiment</option>
|
| 363 |
+
</select>
|
| 364 |
+
</label>
|
| 365 |
+
</div>
|
| 366 |
+
<label>Context or Headline
|
| 367 |
+
<textarea name="context" placeholder="Paste a headline or trade thesis for AI analysis"></textarea>
|
| 368 |
+
</label>
|
| 369 |
+
<button class="primary" type="submit">Generate Guidance</button>
|
| 370 |
+
</form>
|
| 371 |
+
<div class="grid-two">
|
| 372 |
+
<div data-ai-result class="ai-result"></div>
|
| 373 |
+
<div data-sentiment-result></div>
|
| 374 |
+
</div>
|
| 375 |
+
<div class="inline-message inline-info" data-ai-disclaimer>
|
| 376 |
+
Experimental AI output. Not financial advice.
|
| 377 |
+
</div>
|
| 378 |
+
</div>
|
| 379 |
+
</section>
|
| 380 |
+
|
| 381 |
+
<section id="page-news" class="page">
|
| 382 |
+
<div class="section-header">
|
| 383 |
+
<h2 class="section-title">News & Summaries</h2>
|
| 384 |
+
</div>
|
| 385 |
+
<div class="controls-bar">
|
| 386 |
+
<select data-news-range>
|
| 387 |
+
<option value="24h">Last 24h</option>
|
| 388 |
+
<option value="7d">7 Days</option>
|
| 389 |
+
<option value="30d">30 Days</option>
|
| 390 |
+
</select>
|
| 391 |
+
<input type="text" placeholder="Search headline" data-news-search />
|
| 392 |
+
<input type="text" placeholder="Filter symbol (e.g. BTC)" data-news-symbol />
|
| 393 |
+
</div>
|
| 394 |
+
<div class="glass-card">
|
| 395 |
+
<div class="table-wrapper">
|
| 396 |
+
<table>
|
| 397 |
+
<thead>
|
| 398 |
+
<tr>
|
| 399 |
+
<th>Time</th>
|
| 400 |
+
<th>Source</th>
|
| 401 |
+
<th>Title</th>
|
| 402 |
+
<th>Symbols</th>
|
| 403 |
+
<th>Sentiment</th>
|
| 404 |
+
<th>AI</th>
|
| 405 |
+
</tr>
|
| 406 |
+
</thead>
|
| 407 |
+
<tbody data-news-body></tbody>
|
| 408 |
+
</table>
|
| 409 |
+
</div>
|
| 410 |
+
</div>
|
| 411 |
+
<div class="modal-backdrop" data-news-modal>
|
| 412 |
+
<div class="modal">
|
| 413 |
+
<button class="ghost" data-close-news-modal>Close</button>
|
| 414 |
+
<div data-news-modal-content></div>
|
| 415 |
+
</div>
|
| 416 |
+
</div>
|
| 417 |
+
</section>
|
| 418 |
+
|
| 419 |
+
<section id="page-providers" class="page">
|
| 420 |
+
<div class="section-header">
|
| 421 |
+
<h2 class="section-title">Provider Health</h2>
|
| 422 |
+
<button class="ghost" data-provider-refresh>Refresh</button>
|
| 423 |
+
</div>
|
| 424 |
+
<div class="stats-grid" data-provider-summary></div>
|
| 425 |
+
<div class="controls-bar">
|
| 426 |
+
<input type="search" placeholder="Search provider" data-provider-search />
|
| 427 |
+
<select data-provider-category>
|
| 428 |
+
<option value="all">All Categories</option>
|
| 429 |
+
<option value="market">Market Data</option>
|
| 430 |
+
<option value="news">News</option>
|
| 431 |
+
<option value="ai">AI</option>
|
| 432 |
+
</select>
|
| 433 |
+
</div>
|
| 434 |
+
<div class="glass-card">
|
| 435 |
+
<div class="table-wrapper">
|
| 436 |
+
<table>
|
| 437 |
+
<thead>
|
| 438 |
+
<tr>
|
| 439 |
+
<th>Name</th>
|
| 440 |
+
<th>Category</th>
|
| 441 |
+
<th>Status</th>
|
| 442 |
+
<th>Latency</th>
|
| 443 |
+
<th>Details</th>
|
| 444 |
+
</tr>
|
| 445 |
+
</thead>
|
| 446 |
+
<tbody data-providers-table></tbody>
|
| 447 |
+
</table>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
</section>
|
| 451 |
+
|
| 452 |
+
<section id="page-api" class="page">
|
| 453 |
+
<div class="section-header">
|
| 454 |
+
<h2 class="section-title">API Explorer</h2>
|
| 455 |
+
<span class="chip">Test live endpoints</span>
|
| 456 |
+
</div>
|
| 457 |
+
<div class="glass-card">
|
| 458 |
+
<div class="grid-two">
|
| 459 |
+
<label>Endpoint
|
| 460 |
+
<select data-api-endpoint></select>
|
| 461 |
+
</label>
|
| 462 |
+
<label>Method
|
| 463 |
+
<select data-api-method>
|
| 464 |
+
<option value="GET">GET</option>
|
| 465 |
+
<option value="POST">POST</option>
|
| 466 |
+
</select>
|
| 467 |
+
</label>
|
| 468 |
+
<label>Query Params
|
| 469 |
+
<input type="text" placeholder="limit=10&symbol=BTC" data-api-params />
|
| 470 |
+
</label>
|
| 471 |
+
<label>Body (JSON)
|
| 472 |
+
<textarea data-api-body placeholder='{ "text": "Bitcoin" }'></textarea>
|
| 473 |
+
</label>
|
| 474 |
+
</div>
|
| 475 |
+
<p class="text-muted">Path: <span data-api-path></span> — <span data-api-description></span></p>
|
| 476 |
+
<button class="primary" data-api-send>Send Request</button>
|
| 477 |
+
<div class="inline-message" data-api-meta>Ready</div>
|
| 478 |
+
<pre data-api-response class="api-response"></pre>
|
| 479 |
+
</div>
|
| 480 |
+
</section>
|
| 481 |
+
|
| 482 |
+
<section id="page-debug" class="page">
|
| 483 |
+
<div class="section-header">
|
| 484 |
+
<h2 class="section-title">Diagnostics</h2>
|
| 485 |
+
<button class="ghost" data-refresh-health>Refresh</button>
|
| 486 |
+
</div>
|
| 487 |
+
<div class="stats-grid">
|
| 488 |
+
<div class="glass-card">
|
| 489 |
+
<h3>API Health</h3>
|
| 490 |
+
<div class="stat-value" data-health-status>—</div>
|
| 491 |
+
</div>
|
| 492 |
+
<div class="glass-card">
|
| 493 |
+
<h3>Providers</h3>
|
| 494 |
+
<div data-providers class="grid-two"></div>
|
| 495 |
+
</div>
|
| 496 |
+
</div>
|
| 497 |
+
<div class="grid-two">
|
| 498 |
+
<div class="glass-card">
|
| 499 |
+
<h4>Request Log</h4>
|
| 500 |
+
<div class="table-wrapper log-table">
|
| 501 |
+
<table>
|
| 502 |
+
<thead>
|
| 503 |
+
<tr>
|
| 504 |
+
<th>Time</th>
|
| 505 |
+
<th>Method</th>
|
| 506 |
+
<th>Endpoint</th>
|
| 507 |
+
<th>Status</th>
|
| 508 |
+
<th>Latency</th>
|
| 509 |
+
</tr>
|
| 510 |
+
</thead>
|
| 511 |
+
<tbody data-request-log></tbody>
|
| 512 |
+
</table>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
<div class="glass-card">
|
| 516 |
+
<h4>Error Log</h4>
|
| 517 |
+
<div class="table-wrapper log-table">
|
| 518 |
+
<table>
|
| 519 |
+
<thead>
|
| 520 |
+
<tr>
|
| 521 |
+
<th>Time</th>
|
| 522 |
+
<th>Endpoint</th>
|
| 523 |
+
<th>Message</th>
|
| 524 |
+
</tr>
|
| 525 |
+
</thead>
|
| 526 |
+
<tbody data-error-log></tbody>
|
| 527 |
+
</table>
|
| 528 |
+
</div>
|
| 529 |
+
</div>
|
| 530 |
+
</div>
|
| 531 |
+
<div class="glass-card">
|
| 532 |
+
<h4>WebSocket Events</h4>
|
| 533 |
+
<div class="table-wrapper log-table">
|
| 534 |
+
<table>
|
| 535 |
+
<thead>
|
| 536 |
+
<tr>
|
| 537 |
+
<th>Time</th>
|
| 538 |
+
<th>Type</th>
|
| 539 |
+
<th>Detail</th>
|
| 540 |
+
</tr>
|
| 541 |
+
</thead>
|
| 542 |
+
<tbody data-ws-log></tbody>
|
| 543 |
+
</table>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
</section>
|
| 547 |
+
|
| 548 |
+
<section id="page-datasets" class="page">
|
| 549 |
+
<div class="section-header">
|
| 550 |
+
<h2 class="section-title">Datasets & Models</h2>
|
| 551 |
+
</div>
|
| 552 |
+
<div class="grid-two">
|
| 553 |
+
<div class="glass-card">
|
| 554 |
+
<h3>Datasets</h3>
|
| 555 |
+
<div class="table-wrapper">
|
| 556 |
+
<table>
|
| 557 |
+
<thead>
|
| 558 |
+
<tr>
|
| 559 |
+
<th>Name</th>
|
| 560 |
+
<th>Records</th>
|
| 561 |
+
<th>Updated</th>
|
| 562 |
+
<th>Actions</th>
|
| 563 |
+
</tr>
|
| 564 |
+
</thead>
|
| 565 |
+
<tbody data-datasets-body></tbody>
|
| 566 |
+
</table>
|
| 567 |
+
</div>
|
| 568 |
+
</div>
|
| 569 |
+
<div class="glass-card">
|
| 570 |
+
<h3>Models</h3>
|
| 571 |
+
<div class="table-wrapper">
|
| 572 |
+
<table>
|
| 573 |
+
<thead>
|
| 574 |
+
<tr>
|
| 575 |
+
<th>Name</th>
|
| 576 |
+
<th>Task</th>
|
| 577 |
+
<th>Status</th>
|
| 578 |
+
<th>Notes</th>
|
| 579 |
+
</tr>
|
| 580 |
+
</thead>
|
| 581 |
+
<tbody data-models-body></tbody>
|
| 582 |
+
</table>
|
| 583 |
+
</div>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
<div class="glass-card">
|
| 587 |
+
<h4>Test a Model</h4>
|
| 588 |
+
<form data-model-test-form class="grid-two">
|
| 589 |
+
<label>Model
|
| 590 |
+
<select data-model-select name="model"></select>
|
| 591 |
+
</label>
|
| 592 |
+
<label>Input
|
| 593 |
+
<textarea name="input" placeholder="Type a prompt"></textarea>
|
| 594 |
+
</label>
|
| 595 |
+
<button class="primary" type="submit">Run Test</button>
|
| 596 |
+
</form>
|
| 597 |
+
<div data-model-test-output></div>
|
| 598 |
+
</div>
|
| 599 |
+
<div class="modal-backdrop" data-dataset-modal>
|
| 600 |
+
<div class="modal">
|
| 601 |
+
<button class="ghost" data-close-dataset-modal>Close</button>
|
| 602 |
+
<div data-dataset-modal-content></div>
|
| 603 |
+
</div>
|
| 604 |
+
</div>
|
| 605 |
+
</section>
|
| 606 |
+
|
| 607 |
+
<section id="page-settings" class="page">
|
| 608 |
+
<div class="section-header">
|
| 609 |
+
<h2 class="section-title">Settings</h2>
|
| 610 |
+
</div>
|
| 611 |
+
<div class="glass-card">
|
| 612 |
+
<div class="grid-two">
|
| 613 |
+
<label class="input-chip">Light Theme
|
| 614 |
+
<div class="toggle">
|
| 615 |
+
<input type="checkbox" data-theme-toggle />
|
| 616 |
+
<span></span>
|
| 617 |
+
</div>
|
| 618 |
+
</label>
|
| 619 |
+
<label>Market Refresh (sec)
|
| 620 |
+
<input type="number" min="15" step="5" data-market-interval />
|
| 621 |
+
</label>
|
| 622 |
+
<label>News Refresh (sec)
|
| 623 |
+
<input type="number" min="30" step="10" data-news-interval />
|
| 624 |
+
</label>
|
| 625 |
+
<label class="input-chip">Compact Layout
|
| 626 |
+
<div class="toggle">
|
| 627 |
+
<input type="checkbox" data-layout-toggle />
|
| 628 |
+
<span></span>
|
| 629 |
+
</div>
|
| 630 |
+
</label>
|
| 631 |
+
</div>
|
| 632 |
+
</div>
|
| 633 |
+
</section>
|
| 634 |
+
</div>
|
| 635 |
+
</main>
|
| 636 |
+
</div>
|
| 637 |
+
<script type="module" src="static/js/app.js"></script>
|
| 638 |
+
</body>
|
| 639 |
+
</html>
|
backend/routers/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/backend/routers/__pycache__/__init__.cpython-313.pyc and b/backend/routers/__pycache__/__init__.cpython-313.pyc differ
|
|
|
backend/routers/__pycache__/hf_connect.cpython-313.pyc
CHANGED
|
Binary files a/backend/routers/__pycache__/hf_connect.cpython-313.pyc and b/backend/routers/__pycache__/hf_connect.cpython-313.pyc differ
|
|
|
backend/services/__pycache__/hf_client.cpython-313.pyc
CHANGED
|
Binary files a/backend/services/__pycache__/hf_client.cpython-313.pyc and b/backend/services/__pycache__/hf_client.cpython-313.pyc differ
|
|
|
backend/services/__pycache__/hf_registry.cpython-313.pyc
CHANGED
|
Binary files a/backend/services/__pycache__/hf_registry.cpython-313.pyc and b/backend/services/__pycache__/hf_registry.cpython-313.pyc differ
|
|
|
backend/services/__pycache__/local_resource_service.cpython-313.pyc
ADDED
|
Binary file (13.1 kB). View file
|
|
|
backend/services/auto_discovery_service.py
CHANGED
|
@@ -91,7 +91,10 @@ class AutoDiscoveryService:
|
|
| 91 |
if InferenceClient is None:
|
| 92 |
logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
|
| 93 |
else:
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
| 95 |
try:
|
| 96 |
self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
|
| 97 |
logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
|
|
|
|
| 91 |
if InferenceClient is None:
|
| 92 |
logger.warning("huggingface-hub package not available. Auto discovery will use fallback heuristics.")
|
| 93 |
else:
|
| 94 |
+
# Get HF token from environment or use default
|
| 95 |
+
from config import get_settings
|
| 96 |
+
settings = get_settings()
|
| 97 |
+
hf_token = os.getenv("HF_TOKEN") or os.getenv("HF_API_TOKEN") or settings.hf_token or "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"
|
| 98 |
try:
|
| 99 |
self._hf_client = InferenceClient(model=self.hf_model, token=hf_token)
|
| 100 |
logger.info("Auto discovery Hugging Face client initialized with model %s", self.hf_model)
|
backend/services/diagnostics_service.py
CHANGED
|
@@ -260,7 +260,14 @@ class DiagnosticsService:
|
|
| 260 |
|
| 261 |
try:
|
| 262 |
from huggingface_hub import InferenceClient, HfApi
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
# بررسی مدلهای استفاده شده
|
| 266 |
models_to_check = [
|
|
|
|
| 260 |
|
| 261 |
try:
|
| 262 |
from huggingface_hub import InferenceClient, HfApi
|
| 263 |
+
import os
|
| 264 |
+
from config import get_settings
|
| 265 |
+
|
| 266 |
+
# Get HF token from settings or use default
|
| 267 |
+
settings = get_settings()
|
| 268 |
+
hf_token = settings.hf_token or os.getenv("HF_TOKEN") or "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"
|
| 269 |
+
|
| 270 |
+
api = HfApi(token=hf_token)
|
| 271 |
|
| 272 |
# بررسی مدلهای استفاده شده
|
| 273 |
models_to_check = [
|
backend/services/hf_registry.py
CHANGED
|
@@ -8,6 +8,16 @@ HF_API_DATASETS = "https://huggingface.co/api/datasets"
|
|
| 8 |
REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
|
| 9 |
HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
# Curated Crypto Datasets
|
| 12 |
CRYPTO_DATASETS = {
|
| 13 |
"price": [
|
|
@@ -45,68 +55,81 @@ class HFRegistry:
|
|
| 45 |
self.fail_reason: Optional[str] = None
|
| 46 |
|
| 47 |
async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
r = await client.get(url, params=params)
|
| 50 |
r.raise_for_status()
|
| 51 |
return r.json()
|
| 52 |
|
| 53 |
async def refresh(self) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
try:
|
| 55 |
-
# Seed models
|
| 56 |
for name in _SEED_MODELS:
|
| 57 |
self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
|
| 58 |
|
| 59 |
-
# Seed datasets with category metadata
|
| 60 |
for category, dataset_list in CRYPTO_DATASETS.items():
|
| 61 |
for name in dataset_list:
|
| 62 |
self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
|
| 63 |
|
| 64 |
-
|
| 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 |
else:
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
self.datasets[did] = {
|
| 97 |
-
"id": did,
|
| 98 |
-
"likes": d.get("likes"),
|
| 99 |
-
"downloads": d.get("downloads"),
|
| 100 |
-
"tags": d.get("tags") or [],
|
| 101 |
-
"category": category,
|
| 102 |
-
"source": "hub"
|
| 103 |
-
}
|
| 104 |
|
| 105 |
self.last_refresh = time.time()
|
| 106 |
-
self.fail_reason
|
| 107 |
-
|
|
|
|
| 108 |
except Exception as e:
|
| 109 |
-
self.fail_reason = str(e)
|
| 110 |
return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
|
| 111 |
|
| 112 |
def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
|
|
|
|
| 8 |
REFRESH_INTERVAL_SEC = int(os.getenv("HF_REGISTRY_REFRESH_SEC", "21600"))
|
| 9 |
HTTP_TIMEOUT = float(os.getenv("HF_HTTP_TIMEOUT", "8.0"))
|
| 10 |
|
| 11 |
+
HF_MODE = os.getenv("HF_MODE", "off").lower()
|
| 12 |
+
if HF_MODE not in ("off", "public", "auth"):
|
| 13 |
+
HF_MODE = "off"
|
| 14 |
+
|
| 15 |
+
HF_TOKEN = None
|
| 16 |
+
if HF_MODE == "auth":
|
| 17 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 18 |
+
if not HF_TOKEN:
|
| 19 |
+
HF_MODE = "off"
|
| 20 |
+
|
| 21 |
# Curated Crypto Datasets
|
| 22 |
CRYPTO_DATASETS = {
|
| 23 |
"price": [
|
|
|
|
| 55 |
self.fail_reason: Optional[str] = None
|
| 56 |
|
| 57 |
async def _hf_json(self, url: str, params: Dict[str, Any]) -> Any:
|
| 58 |
+
headers = {}
|
| 59 |
+
if HF_MODE == "auth" and HF_TOKEN:
|
| 60 |
+
headers["Authorization"] = f"Bearer {HF_TOKEN}"
|
| 61 |
+
|
| 62 |
+
async with httpx.AsyncClient(timeout=HTTP_TIMEOUT, headers=headers) as client:
|
| 63 |
r = await client.get(url, params=params)
|
| 64 |
r.raise_for_status()
|
| 65 |
return r.json()
|
| 66 |
|
| 67 |
async def refresh(self) -> Dict[str, Any]:
|
| 68 |
+
if HF_MODE == "off":
|
| 69 |
+
self.fail_reason = "HF_MODE=off"
|
| 70 |
+
return {"ok": False, "error": "HF_MODE=off", "models": 0, "datasets": 0}
|
| 71 |
+
|
| 72 |
try:
|
|
|
|
| 73 |
for name in _SEED_MODELS:
|
| 74 |
self.models.setdefault(name, {"id": name, "source": "seed", "pipeline_tag": "sentiment-analysis"})
|
| 75 |
|
|
|
|
| 76 |
for category, dataset_list in CRYPTO_DATASETS.items():
|
| 77 |
for name in dataset_list:
|
| 78 |
self.datasets.setdefault(name, {"id": name, "source": "seed", "category": category, "tags": ["crypto", category]})
|
| 79 |
|
| 80 |
+
if HF_MODE in ("public", "auth"):
|
| 81 |
+
try:
|
| 82 |
+
q_sent = {"pipeline_tag": "sentiment-analysis", "search": "crypto", "limit": 50}
|
| 83 |
+
models = await self._hf_json(HF_API_MODELS, q_sent)
|
| 84 |
+
for m in models or []:
|
| 85 |
+
mid = m.get("modelId") or m.get("id") or m.get("name")
|
| 86 |
+
if not mid: continue
|
| 87 |
+
self.models[mid] = {
|
| 88 |
+
"id": mid,
|
| 89 |
+
"pipeline_tag": m.get("pipeline_tag"),
|
| 90 |
+
"likes": m.get("likes"),
|
| 91 |
+
"downloads": m.get("downloads"),
|
| 92 |
+
"tags": m.get("tags") or [],
|
| 93 |
+
"source": "hub"
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
q_crypto = {"search": "crypto", "limit": 100}
|
| 97 |
+
datasets = await self._hf_json(HF_API_DATASETS, q_crypto)
|
| 98 |
+
for d in datasets or []:
|
| 99 |
+
did = d.get("id") or d.get("name")
|
| 100 |
+
if not did: continue
|
| 101 |
+
category = "other"
|
| 102 |
+
tags_str = " ".join(d.get("tags") or []).lower()
|
| 103 |
+
name_lower = did.lower()
|
| 104 |
+
if "price" in tags_str or "ohlc" in tags_str or "price" in name_lower:
|
| 105 |
+
category = "price"
|
| 106 |
+
elif "news" in tags_str or "news" in name_lower:
|
| 107 |
+
if "label" in tags_str or "sentiment" in tags_str:
|
| 108 |
+
category = "news_labeled"
|
| 109 |
+
else:
|
| 110 |
+
category = "news_raw"
|
| 111 |
+
|
| 112 |
+
self.datasets[did] = {
|
| 113 |
+
"id": did,
|
| 114 |
+
"likes": d.get("likes"),
|
| 115 |
+
"downloads": d.get("downloads"),
|
| 116 |
+
"tags": d.get("tags") or [],
|
| 117 |
+
"category": category,
|
| 118 |
+
"source": "hub"
|
| 119 |
+
}
|
| 120 |
+
except Exception as e:
|
| 121 |
+
error_msg = str(e)[:200]
|
| 122 |
+
if "401" in error_msg or "unauthorized" in error_msg.lower():
|
| 123 |
+
self.fail_reason = "Authentication failed"
|
| 124 |
else:
|
| 125 |
+
self.fail_reason = error_msg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
self.last_refresh = time.time()
|
| 128 |
+
if self.fail_reason is None:
|
| 129 |
+
return {"ok": True, "models": len(self.models), "datasets": len(self.datasets)}
|
| 130 |
+
return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
|
| 131 |
except Exception as e:
|
| 132 |
+
self.fail_reason = str(e)[:200]
|
| 133 |
return {"ok": False, "error": self.fail_reason, "models": len(self.models), "datasets": len(self.datasets)}
|
| 134 |
|
| 135 |
def list(self, kind: Literal["models","datasets"]="models", category: Optional[str]=None) -> List[Dict[str, Any]]:
|
backend/services/local_resource_service.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from copy import deepcopy
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LocalResourceService:
|
| 10 |
+
"""Centralized loader for the unified fallback registry."""
|
| 11 |
+
|
| 12 |
+
def __init__(self, resource_path: Path):
|
| 13 |
+
self.resource_path = Path(resource_path)
|
| 14 |
+
self._raw_data: Optional[Dict[str, Any]] = None
|
| 15 |
+
self._assets: Dict[str, Dict[str, Any]] = {}
|
| 16 |
+
self._market_overview: Dict[str, Any] = {}
|
| 17 |
+
self._logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
# --------------------------------------------------------------------- #
|
| 20 |
+
# Loading helpers
|
| 21 |
+
# --------------------------------------------------------------------- #
|
| 22 |
+
def _ensure_loaded(self) -> None:
|
| 23 |
+
if self._raw_data is not None:
|
| 24 |
+
return
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
with self.resource_path.open("r", encoding="utf-8") as handle:
|
| 28 |
+
data = json.load(handle)
|
| 29 |
+
except FileNotFoundError:
|
| 30 |
+
self._logger.warning("Fallback registry %s not found", self.resource_path)
|
| 31 |
+
data = {}
|
| 32 |
+
except json.JSONDecodeError as exc:
|
| 33 |
+
self._logger.error("Invalid fallback registry JSON: %s", exc)
|
| 34 |
+
data = {}
|
| 35 |
+
|
| 36 |
+
fallback_data = data.get("fallback_data") or {}
|
| 37 |
+
assets = fallback_data.get("assets") or {}
|
| 38 |
+
normalized_assets: Dict[str, Dict[str, Any]] = {}
|
| 39 |
+
|
| 40 |
+
for key, details in assets.items():
|
| 41 |
+
symbol = str(details.get("symbol") or key).upper()
|
| 42 |
+
asset_copy = deepcopy(details)
|
| 43 |
+
asset_copy["symbol"] = symbol
|
| 44 |
+
normalized_assets[symbol] = asset_copy
|
| 45 |
+
|
| 46 |
+
self._raw_data = data
|
| 47 |
+
self._assets = normalized_assets
|
| 48 |
+
self._market_overview = deepcopy(fallback_data.get("market_overview") or {})
|
| 49 |
+
|
| 50 |
+
def refresh(self) -> None:
|
| 51 |
+
"""Force reload from disk (used in tests)."""
|
| 52 |
+
self._raw_data = None
|
| 53 |
+
self._assets = {}
|
| 54 |
+
self._market_overview = {}
|
| 55 |
+
self._ensure_loaded()
|
| 56 |
+
|
| 57 |
+
# --------------------------------------------------------------------- #
|
| 58 |
+
# Registry level helpers
|
| 59 |
+
# --------------------------------------------------------------------- #
|
| 60 |
+
def get_registry(self) -> Dict[str, Any]:
|
| 61 |
+
self._ensure_loaded()
|
| 62 |
+
return deepcopy(self._raw_data or {})
|
| 63 |
+
|
| 64 |
+
def get_supported_symbols(self) -> List[str]:
|
| 65 |
+
self._ensure_loaded()
|
| 66 |
+
return sorted(self._assets.keys())
|
| 67 |
+
|
| 68 |
+
def has_fallback_data(self) -> bool:
|
| 69 |
+
self._ensure_loaded()
|
| 70 |
+
return bool(self._assets)
|
| 71 |
+
|
| 72 |
+
# --------------------------------------------------------------------- #
|
| 73 |
+
# Market data helpers
|
| 74 |
+
# --------------------------------------------------------------------- #
|
| 75 |
+
def _asset_to_market_record(self, asset: Dict[str, Any]) -> Dict[str, Any]:
|
| 76 |
+
price = asset.get("price", {})
|
| 77 |
+
return {
|
| 78 |
+
"id": asset.get("slug") or asset.get("symbol", "").lower(),
|
| 79 |
+
"symbol": asset.get("symbol"),
|
| 80 |
+
"name": asset.get("name"),
|
| 81 |
+
"current_price": price.get("current_price"),
|
| 82 |
+
"market_cap": price.get("market_cap"),
|
| 83 |
+
"market_cap_rank": asset.get("market_cap_rank"),
|
| 84 |
+
"total_volume": price.get("total_volume"),
|
| 85 |
+
"price_change_24h": price.get("price_change_24h"),
|
| 86 |
+
"price_change_percentage_24h": price.get("price_change_percentage_24h"),
|
| 87 |
+
"high_24h": price.get("high_24h"),
|
| 88 |
+
"low_24h": price.get("low_24h"),
|
| 89 |
+
"last_updated": price.get("last_updated"),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
def get_top_prices(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 93 |
+
self._ensure_loaded()
|
| 94 |
+
if not self._assets:
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
sorted_assets = sorted(
|
| 98 |
+
self._assets.values(),
|
| 99 |
+
key=lambda x: (x.get("market_cap_rank") or 9999, -(x.get("price", {}).get("market_cap") or 0)),
|
| 100 |
+
)
|
| 101 |
+
selected = sorted_assets[: max(1, limit)]
|
| 102 |
+
return [self._asset_to_market_record(asset) for asset in selected]
|
| 103 |
+
|
| 104 |
+
def get_prices_for_symbols(self, symbols: List[str]) -> List[Dict[str, Any]]:
|
| 105 |
+
self._ensure_loaded()
|
| 106 |
+
if not symbols or not self._assets:
|
| 107 |
+
return []
|
| 108 |
+
|
| 109 |
+
results: List[Dict[str, Any]] = []
|
| 110 |
+
for raw_symbol in symbols:
|
| 111 |
+
symbol = str(raw_symbol or "").upper()
|
| 112 |
+
asset = self._assets.get(symbol)
|
| 113 |
+
if asset:
|
| 114 |
+
results.append(self._asset_to_market_record(asset))
|
| 115 |
+
return results
|
| 116 |
+
|
| 117 |
+
def get_ticker_snapshot(self, symbol: str) -> Optional[Dict[str, Any]]:
|
| 118 |
+
self._ensure_loaded()
|
| 119 |
+
asset = self._assets.get(str(symbol or "").upper())
|
| 120 |
+
if not asset:
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
price = asset.get("price", {})
|
| 124 |
+
return {
|
| 125 |
+
"symbol": asset.get("symbol"),
|
| 126 |
+
"price": price.get("current_price"),
|
| 127 |
+
"price_change_24h": price.get("price_change_24h"),
|
| 128 |
+
"price_change_percent_24h": price.get("price_change_percentage_24h"),
|
| 129 |
+
"high_24h": price.get("high_24h"),
|
| 130 |
+
"low_24h": price.get("low_24h"),
|
| 131 |
+
"volume_24h": price.get("total_volume"),
|
| 132 |
+
"quote_volume_24h": price.get("total_volume"),
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
def get_market_overview(self) -> Dict[str, Any]:
|
| 136 |
+
self._ensure_loaded()
|
| 137 |
+
if not self._assets:
|
| 138 |
+
return {}
|
| 139 |
+
|
| 140 |
+
overview = deepcopy(self._market_overview)
|
| 141 |
+
if not overview:
|
| 142 |
+
total_market_cap = sum(
|
| 143 |
+
(asset.get("price", {}) or {}).get("market_cap") or 0 for asset in self._assets.values()
|
| 144 |
+
)
|
| 145 |
+
total_volume = sum(
|
| 146 |
+
(asset.get("price", {}) or {}).get("total_volume") or 0 for asset in self._assets.values()
|
| 147 |
+
)
|
| 148 |
+
btc = self._assets.get("BTC", {})
|
| 149 |
+
btc_cap = (btc.get("price", {}) or {}).get("market_cap") or 0
|
| 150 |
+
overview = {
|
| 151 |
+
"total_market_cap": total_market_cap,
|
| 152 |
+
"total_volume_24h": total_volume,
|
| 153 |
+
"btc_dominance": (btc_cap / total_market_cap * 100) if total_market_cap else 0,
|
| 154 |
+
"active_cryptocurrencies": len(self._assets),
|
| 155 |
+
"markets": 500,
|
| 156 |
+
"market_cap_change_percentage_24h": 0,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
# Enrich with derived leaderboards
|
| 160 |
+
gainers = sorted(
|
| 161 |
+
self._assets.values(),
|
| 162 |
+
key=lambda asset: (asset.get("price", {}) or {}).get("price_change_percentage_24h") or 0,
|
| 163 |
+
reverse=True,
|
| 164 |
+
)[:5]
|
| 165 |
+
losers = sorted(
|
| 166 |
+
self._assets.values(),
|
| 167 |
+
key=lambda asset: (asset.get("price", {}) or {}).get("price_change_percentage_24h") or 0,
|
| 168 |
+
)[:5]
|
| 169 |
+
volumes = sorted(
|
| 170 |
+
self._assets.values(),
|
| 171 |
+
key=lambda asset: (asset.get("price", {}) or {}).get("total_volume") or 0,
|
| 172 |
+
reverse=True,
|
| 173 |
+
)[:5]
|
| 174 |
+
|
| 175 |
+
overview["top_gainers"] = [self._asset_to_market_record(asset) for asset in gainers]
|
| 176 |
+
overview["top_losers"] = [self._asset_to_market_record(asset) for asset in losers]
|
| 177 |
+
overview["top_by_volume"] = [self._asset_to_market_record(asset) for asset in volumes]
|
| 178 |
+
overview["timestamp"] = overview.get("timestamp") or datetime.utcnow().isoformat()
|
| 179 |
+
|
| 180 |
+
return overview
|
| 181 |
+
|
| 182 |
+
def get_ohlcv(self, symbol: str, interval: str = "1h", limit: int = 100) -> List[Dict[str, Any]]:
|
| 183 |
+
self._ensure_loaded()
|
| 184 |
+
asset = self._assets.get(str(symbol or "").upper())
|
| 185 |
+
if not asset:
|
| 186 |
+
return []
|
| 187 |
+
|
| 188 |
+
ohlcv = (asset.get("ohlcv") or {}).get(interval) or []
|
| 189 |
+
if not ohlcv and interval != "1h":
|
| 190 |
+
# Provide 1h data for other intervals when nothing else is present
|
| 191 |
+
ohlcv = (asset.get("ohlcv") or {}).get("1h") or []
|
| 192 |
+
|
| 193 |
+
if limit and ohlcv:
|
| 194 |
+
return deepcopy(ohlcv[-limit:])
|
| 195 |
+
return deepcopy(ohlcv)
|
| 196 |
+
|
| 197 |
+
# --------------------------------------------------------------------- #
|
| 198 |
+
# Convenience helpers for testing / diagnostics
|
| 199 |
+
# --------------------------------------------------------------------- #
|
| 200 |
+
def describe(self) -> Dict[str, Any]:
|
| 201 |
+
"""Simple snapshot used in diagnostics/tests."""
|
| 202 |
+
self._ensure_loaded()
|
| 203 |
+
return {
|
| 204 |
+
"resource_path": str(self.resource_path),
|
| 205 |
+
"assets": len(self._assets),
|
| 206 |
+
"supported_symbols": self.get_supported_symbols(),
|
| 207 |
+
}
|
check_server.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Check if the server is running and accessible
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import socket
|
| 7 |
+
import requests
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
def check_port(host='localhost', port=7860):
|
| 11 |
+
"""Check if a port is open"""
|
| 12 |
+
try:
|
| 13 |
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
| 14 |
+
sock.settimeout(1)
|
| 15 |
+
result = sock.connect_ex((host, port))
|
| 16 |
+
sock.close()
|
| 17 |
+
return result == 0
|
| 18 |
+
except Exception as e:
|
| 19 |
+
print(f"Error checking port: {e}")
|
| 20 |
+
return False
|
| 21 |
+
|
| 22 |
+
def check_endpoint(url):
|
| 23 |
+
"""Check if an endpoint is accessible"""
|
| 24 |
+
try:
|
| 25 |
+
response = requests.get(url, timeout=2)
|
| 26 |
+
return response.status_code, response.text[:100]
|
| 27 |
+
except requests.exceptions.ConnectionError:
|
| 28 |
+
return None, "Connection refused - server not running"
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return None, str(e)
|
| 31 |
+
|
| 32 |
+
print("=" * 70)
|
| 33 |
+
print("Server Diagnostic Check")
|
| 34 |
+
print("=" * 70)
|
| 35 |
+
|
| 36 |
+
# Check if port is open
|
| 37 |
+
print("\n1. Checking if port 7860 is open...")
|
| 38 |
+
if check_port('localhost', 7860):
|
| 39 |
+
print(" ✓ Port 7860 is open")
|
| 40 |
+
else:
|
| 41 |
+
print(" ✗ Port 7860 is not open - server is NOT running")
|
| 42 |
+
print("\n SOLUTION: Start the server with:")
|
| 43 |
+
print(" python main.py")
|
| 44 |
+
sys.exit(1)
|
| 45 |
+
|
| 46 |
+
# Check if it's the correct server
|
| 47 |
+
print("\n2. Checking if correct server is running...")
|
| 48 |
+
status, text = check_endpoint('http://localhost:7860/health')
|
| 49 |
+
if status == 200:
|
| 50 |
+
print(" ✓ Health endpoint responds (correct server)")
|
| 51 |
+
elif status == 404:
|
| 52 |
+
print(" ✗ Health endpoint returns 404")
|
| 53 |
+
print(" WARNING: Something is running on port 7860, but it's not the FastAPI server!")
|
| 54 |
+
print(" This might be python -m http.server or another static file server.")
|
| 55 |
+
print("\n SOLUTION:")
|
| 56 |
+
print(" 1. Stop the current server (Ctrl+C in the terminal running it)")
|
| 57 |
+
print(" 2. Start the correct server: python main.py")
|
| 58 |
+
sys.exit(1)
|
| 59 |
+
else:
|
| 60 |
+
print(f" ✗ Health endpoint error: {status} - {text}")
|
| 61 |
+
sys.exit(1)
|
| 62 |
+
|
| 63 |
+
# Check API endpoints
|
| 64 |
+
print("\n3. Checking API endpoints...")
|
| 65 |
+
endpoints = [
|
| 66 |
+
'/api/market',
|
| 67 |
+
'/api/coins/top?limit=10',
|
| 68 |
+
'/api/news/latest?limit=20',
|
| 69 |
+
'/api/sentiment',
|
| 70 |
+
'/api/providers/config',
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
all_ok = True
|
| 74 |
+
for endpoint in endpoints:
|
| 75 |
+
status, text = check_endpoint(f'http://localhost:7860{endpoint}')
|
| 76 |
+
if status == 200:
|
| 77 |
+
print(f" ✓ {endpoint}")
|
| 78 |
+
else:
|
| 79 |
+
print(f" ✗ {endpoint} - Status: {status}, Error: {text}")
|
| 80 |
+
all_ok = False
|
| 81 |
+
|
| 82 |
+
if not all_ok:
|
| 83 |
+
print("\n WARNING: Some endpoints are not working!")
|
| 84 |
+
print(" The server is running but routes may not be registered correctly.")
|
| 85 |
+
print(" Try restarting the server: python main.py")
|
| 86 |
+
|
| 87 |
+
# Check WebSocket (can't easily test, but check if route exists)
|
| 88 |
+
print("\n4. WebSocket endpoint:")
|
| 89 |
+
print(" The /ws endpoint should be available at ws://localhost:7860/ws")
|
| 90 |
+
print(" (Cannot test WebSocket from this script)")
|
| 91 |
+
|
| 92 |
+
print("\n" + "=" * 70)
|
| 93 |
+
if all_ok:
|
| 94 |
+
print("✓ Server is running correctly!")
|
| 95 |
+
print(" Access the dashboard at: http://localhost:7860/")
|
| 96 |
+
print(" API docs at: http://localhost:7860/docs")
|
| 97 |
+
else:
|
| 98 |
+
print("⚠ Server is running but some endpoints have issues")
|
| 99 |
+
print(" Try restarting: python main.py")
|
| 100 |
+
print("=" * 70)
|
| 101 |
+
|
collectors/__pycache__/aggregator.cpython-313.pyc
CHANGED
|
Binary files a/collectors/__pycache__/aggregator.cpython-313.pyc and b/collectors/__pycache__/aggregator.cpython-313.pyc differ
|
|
|
collectors/aggregator.py
CHANGED
|
@@ -88,6 +88,8 @@ class MarketDataCollector:
|
|
| 88 |
self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
|
| 89 |
self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
|
| 90 |
self.timeout = 15.0
|
|
|
|
|
|
|
| 91 |
|
| 92 |
async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
| 93 |
provider = self.registry.providers.get(provider_key)
|
|
@@ -95,8 +97,54 @@ class MarketDataCollector:
|
|
| 95 |
raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
|
| 96 |
|
| 97 |
url = provider["base_url"].rstrip("/") + path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
|
| 99 |
response = await client.get(url, params=params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if response.status_code != 200:
|
| 101 |
raise CollectorError(
|
| 102 |
f"{provider_key} request failed with HTTP {response.status_code}",
|
|
@@ -105,14 +153,31 @@ class MarketDataCollector:
|
|
| 105 |
)
|
| 106 |
return response.json()
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 109 |
cache_key = f"top_coins:{limit}"
|
| 110 |
cached = await self.cache.get(cache_key)
|
| 111 |
if cached:
|
| 112 |
return cached
|
| 113 |
|
| 114 |
-
|
|
|
|
| 115 |
last_error: Optional[Exception] = None
|
|
|
|
|
|
|
| 116 |
for provider in providers:
|
| 117 |
try:
|
| 118 |
if provider == "coingecko":
|
|
@@ -160,11 +225,53 @@ class MarketDataCollector:
|
|
| 160 |
]
|
| 161 |
await self.cache.set(cache_key, coins)
|
| 162 |
return coins
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
except Exception as exc: # pragma: no cover - network heavy
|
| 164 |
last_error = exc
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
raise CollectorError("Unable to fetch top coins", provider=str(last_error))
|
| 168 |
|
| 169 |
async def _coin_id(self, symbol: str) -> str:
|
| 170 |
symbol_lower = symbol.lower()
|
|
@@ -365,7 +472,9 @@ class ProviderStatusCollector:
|
|
| 365 |
"latency_ms": latency,
|
| 366 |
}
|
| 367 |
except Exception as exc: # pragma: no cover - network heavy
|
| 368 |
-
|
|
|
|
|
|
|
| 369 |
return {
|
| 370 |
"provider_id": provider_id,
|
| 371 |
"name": data.get("name", provider_id),
|
|
|
|
| 88 |
self._symbol_map = {symbol.lower(): coin_id for coin_id, symbol in COIN_SYMBOL_MAPPING.items()}
|
| 89 |
self.headers = {"User-Agent": settings.user_agent or USER_AGENT}
|
| 90 |
self.timeout = 15.0
|
| 91 |
+
self._last_error_log: Dict[str, float] = {} # Track last error log time per provider
|
| 92 |
+
self._error_log_throttle = 60.0 # Only log same error once per 60 seconds
|
| 93 |
|
| 94 |
async def _request(self, provider_key: str, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
| 95 |
provider = self.registry.providers.get(provider_key)
|
|
|
|
| 97 |
raise CollectorError(f"Provider {provider_key} not configured", provider=provider_key)
|
| 98 |
|
| 99 |
url = provider["base_url"].rstrip("/") + path
|
| 100 |
+
|
| 101 |
+
# Rate limit tracking per provider
|
| 102 |
+
if not hasattr(self, '_rate_limit_timestamps'):
|
| 103 |
+
self._rate_limit_timestamps: Dict[str, List[float]] = {}
|
| 104 |
+
if provider_key not in self._rate_limit_timestamps:
|
| 105 |
+
self._rate_limit_timestamps[provider_key] = []
|
| 106 |
+
|
| 107 |
+
# Get rate limits from provider config
|
| 108 |
+
rate_limit_rpm = provider.get("rate_limit", {}).get("requests_per_minute", 30)
|
| 109 |
+
if rate_limit_rpm and len(self._rate_limit_timestamps[provider_key]) >= rate_limit_rpm:
|
| 110 |
+
# Check if oldest request is older than 1 minute
|
| 111 |
+
oldest_time = self._rate_limit_timestamps[provider_key][0]
|
| 112 |
+
if time.time() - oldest_time < 60:
|
| 113 |
+
wait_time = 60 - (time.time() - oldest_time) + 1
|
| 114 |
+
if self._should_log_error(provider_key, "rate_limit_wait"):
|
| 115 |
+
logger.warning(f"Rate limiting {provider_key}, waiting {wait_time:.1f}s")
|
| 116 |
+
await asyncio.sleep(wait_time)
|
| 117 |
+
# Clean old timestamps
|
| 118 |
+
cutoff = time.time() - 60
|
| 119 |
+
self._rate_limit_timestamps[provider_key] = [
|
| 120 |
+
ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
async with httpx.AsyncClient(timeout=self.timeout, headers=self.headers) as client:
|
| 124 |
response = await client.get(url, params=params)
|
| 125 |
+
|
| 126 |
+
# Record request timestamp
|
| 127 |
+
self._rate_limit_timestamps[provider_key].append(time.time())
|
| 128 |
+
# Keep only last minute of timestamps
|
| 129 |
+
cutoff = time.time() - 60
|
| 130 |
+
self._rate_limit_timestamps[provider_key] = [
|
| 131 |
+
ts for ts in self._rate_limit_timestamps[provider_key] if ts > cutoff
|
| 132 |
+
]
|
| 133 |
+
|
| 134 |
+
# Handle HTTP 429 (Rate Limit) with exponential backoff
|
| 135 |
+
if response.status_code == 429:
|
| 136 |
+
retry_after = int(response.headers.get("Retry-After", "60"))
|
| 137 |
+
error_msg = f"{provider_key} rate limited (HTTP 429), retry after {retry_after}s"
|
| 138 |
+
|
| 139 |
+
if self._should_log_error(provider_key, "HTTP 429"):
|
| 140 |
+
logger.warning(error_msg)
|
| 141 |
+
|
| 142 |
+
raise CollectorError(
|
| 143 |
+
error_msg,
|
| 144 |
+
provider=provider_key,
|
| 145 |
+
status_code=429,
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
if response.status_code != 200:
|
| 149 |
raise CollectorError(
|
| 150 |
f"{provider_key} request failed with HTTP {response.status_code}",
|
|
|
|
| 153 |
)
|
| 154 |
return response.json()
|
| 155 |
|
| 156 |
+
def _should_log_error(self, provider: str, error_msg: str) -> bool:
|
| 157 |
+
"""Check if error should be logged (throttle repeated errors)."""
|
| 158 |
+
error_key = f"{provider}:{error_msg}"
|
| 159 |
+
now = time.time()
|
| 160 |
+
last_log_time = self._last_error_log.get(error_key, 0)
|
| 161 |
+
|
| 162 |
+
if now - last_log_time > self._error_log_throttle:
|
| 163 |
+
self._last_error_log[error_key] = now
|
| 164 |
+
# Clean up old entries (keep only last hour)
|
| 165 |
+
cutoff = now - 3600
|
| 166 |
+
self._last_error_log = {k: v for k, v in self._last_error_log.items() if v > cutoff}
|
| 167 |
+
return True
|
| 168 |
+
return False
|
| 169 |
+
|
| 170 |
async def get_top_coins(self, limit: int = 10) -> List[Dict[str, Any]]:
|
| 171 |
cache_key = f"top_coins:{limit}"
|
| 172 |
cached = await self.cache.get(cache_key)
|
| 173 |
if cached:
|
| 174 |
return cached
|
| 175 |
|
| 176 |
+
# Provider list with priority order (add more fallbacks from resource files)
|
| 177 |
+
providers = ["coingecko", "coincap", "coinpaprika"]
|
| 178 |
last_error: Optional[Exception] = None
|
| 179 |
+
last_error_details: Optional[str] = None
|
| 180 |
+
|
| 181 |
for provider in providers:
|
| 182 |
try:
|
| 183 |
if provider == "coingecko":
|
|
|
|
| 225 |
]
|
| 226 |
await self.cache.set(cache_key, coins)
|
| 227 |
return coins
|
| 228 |
+
|
| 229 |
+
if provider == "coinpaprika":
|
| 230 |
+
data = await self._request("coinpaprika", "/tickers", {"quotes": "USD", "limit": limit})
|
| 231 |
+
coins = [
|
| 232 |
+
{
|
| 233 |
+
"name": item.get("name"),
|
| 234 |
+
"symbol": item.get("symbol", "").upper(),
|
| 235 |
+
"price": float(item.get("quotes", {}).get("USD", {}).get("price", 0)),
|
| 236 |
+
"change_24h": float(item.get("quotes", {}).get("USD", {}).get("percent_change_24h", 0)),
|
| 237 |
+
"market_cap": float(item.get("quotes", {}).get("USD", {}).get("market_cap", 0)),
|
| 238 |
+
"volume_24h": float(item.get("quotes", {}).get("USD", {}).get("volume_24h", 0)),
|
| 239 |
+
"rank": int(item.get("rank", 0)),
|
| 240 |
+
"last_updated": item.get("last_updated"),
|
| 241 |
+
}
|
| 242 |
+
for item in data[:limit] if item.get("quotes", {}).get("USD")
|
| 243 |
+
]
|
| 244 |
+
await self.cache.set(cache_key, coins)
|
| 245 |
+
return coins
|
| 246 |
except Exception as exc: # pragma: no cover - network heavy
|
| 247 |
last_error = exc
|
| 248 |
+
error_msg = str(exc) if str(exc) else repr(exc)
|
| 249 |
+
error_type = type(exc).__name__
|
| 250 |
+
|
| 251 |
+
# Extract HTTP status code if available
|
| 252 |
+
if hasattr(exc, 'status_code'):
|
| 253 |
+
status_code = exc.status_code
|
| 254 |
+
error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
|
| 255 |
+
elif isinstance(exc, CollectorError) and hasattr(exc, 'status_code') and exc.status_code:
|
| 256 |
+
status_code = exc.status_code
|
| 257 |
+
error_msg = f"HTTP {status_code}: {error_msg}" if error_msg else f"HTTP {status_code}"
|
| 258 |
+
|
| 259 |
+
# Ensure we always have a meaningful error message
|
| 260 |
+
if not error_msg or error_msg.strip() == "":
|
| 261 |
+
error_msg = f"{error_type} (no details available)"
|
| 262 |
+
|
| 263 |
+
last_error_details = f"{error_type}: {error_msg}"
|
| 264 |
+
|
| 265 |
+
# Throttle error logging to prevent spam
|
| 266 |
+
error_key_for_logging = error_msg or error_type
|
| 267 |
+
if self._should_log_error(provider, error_key_for_logging):
|
| 268 |
+
logger.warning(
|
| 269 |
+
"Provider %s failed: %s (error logged, will suppress similar errors for 60s)",
|
| 270 |
+
provider,
|
| 271 |
+
last_error_details
|
| 272 |
+
)
|
| 273 |
|
| 274 |
+
raise CollectorError(f"Unable to fetch top coins from any provider. Last error: {last_error_details or 'Unknown'}", provider=str(last_error) if last_error else None)
|
| 275 |
|
| 276 |
async def _coin_id(self, symbol: str) -> str:
|
| 277 |
symbol_lower = symbol.lower()
|
|
|
|
| 472 |
"latency_ms": latency,
|
| 473 |
}
|
| 474 |
except Exception as exc: # pragma: no cover - network heavy
|
| 475 |
+
error_msg = str(exc)
|
| 476 |
+
error_type = type(exc).__name__
|
| 477 |
+
logger.warning("Provider %s health check failed: %s: %s", provider_id, error_type, error_msg)
|
| 478 |
return {
|
| 479 |
"provider_id": provider_id,
|
| 480 |
"name": data.get("name", provider_id),
|
config.js
CHANGED
|
@@ -1,146 +1,389 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
| 3 |
-
*
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
//
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 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 |
-
|
| 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 |
-
method: 'POST',
|
| 126 |
-
headers: {
|
| 127 |
-
'Content-Type': 'application/json',
|
| 128 |
-
},
|
| 129 |
-
body: JSON.stringify(body),
|
| 130 |
-
});
|
| 131 |
-
},
|
| 132 |
-
};
|
| 133 |
-
})();
|
| 134 |
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
| 138 |
}
|
| 139 |
|
| 140 |
-
//
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
+
* ═══════════════════════════════════════════════════════════════════
|
| 3 |
+
* CONFIGURATION FILE
|
| 4 |
+
* Dashboard Settings - Easy Customization
|
| 5 |
+
* ═══════════════════════════════════════════════════════════════════
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
// 🔧 Main Backend Settings
|
| 9 |
+
window.DASHBOARD_CONFIG = {
|
| 10 |
+
|
| 11 |
+
// ═══════════════════════════════════════════════════════════════
|
| 12 |
+
// API and WebSocket URLs
|
| 13 |
+
// ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
// Auto-detect localhost and use port 7860, otherwise use current origin
|
| 16 |
+
BACKEND_URL: (() => {
|
| 17 |
+
const hostname = window.location.hostname;
|
| 18 |
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 19 |
+
return `http://${hostname}:7860`;
|
| 20 |
}
|
| 21 |
+
return window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 22 |
+
})(),
|
| 23 |
+
WS_URL: (() => {
|
| 24 |
+
const hostname = window.location.hostname;
|
| 25 |
+
let backendUrl;
|
| 26 |
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
| 27 |
+
backendUrl = `http://${hostname}:7860`;
|
| 28 |
+
} else {
|
| 29 |
+
backendUrl = window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space';
|
| 30 |
+
}
|
| 31 |
+
return backendUrl.replace('http://', 'ws://').replace('https://', 'wss://') + '/ws';
|
| 32 |
+
})(),
|
| 33 |
|
| 34 |
+
// ⏱️ Update Timing (milliseconds)
|
| 35 |
+
UPDATE_INTERVAL: 30000, // Every 30 seconds
|
| 36 |
+
CACHE_TTL: 60000, // 1 minute
|
| 37 |
+
HEARTBEAT_INTERVAL: 30000, // 30 seconds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
+
// 🔄 Reconnection Settings
|
| 40 |
+
MAX_RECONNECT_ATTEMPTS: 5,
|
| 41 |
+
RECONNECT_DELAY: 3000, // 3 seconds
|
| 42 |
+
|
| 43 |
+
// ═══════════════════════════════════════════════════════════════
|
| 44 |
+
// Display Settings
|
| 45 |
+
// ═══════════════════════════════════════════════════════════════
|
| 46 |
+
|
| 47 |
+
// Number of items to display
|
| 48 |
+
MAX_COINS_DISPLAY: 20, // Number of coins in table
|
| 49 |
+
MAX_NEWS_DISPLAY: 20, // Number of news items
|
| 50 |
+
MAX_TRENDING_DISPLAY: 10, // Number of trending items
|
| 51 |
+
|
| 52 |
+
// Table settings
|
| 53 |
+
TABLE_ROWS_PER_PAGE: 10,
|
| 54 |
+
|
| 55 |
+
// ═══════════════════════════════════════════════════════════════
|
| 56 |
+
// Chart Settings
|
| 57 |
+
// ═══════════════════════════════════════════════════════════════
|
| 58 |
+
|
| 59 |
+
CHART: {
|
| 60 |
+
DEFAULT_SYMBOL: 'BTCUSDT',
|
| 61 |
+
DEFAULT_INTERVAL: '1h',
|
| 62 |
+
AVAILABLE_INTERVALS: ['1m', '5m', '15m', '1h', '4h', '1d'],
|
| 63 |
+
THEME: 'dark',
|
| 64 |
+
},
|
| 65 |
+
|
| 66 |
+
// ═══════════════════════════════════════════════════════════════
|
| 67 |
+
// AI Settings
|
| 68 |
+
// ═══════════════════════════════════════════════════════════════
|
| 69 |
+
|
| 70 |
+
AI: {
|
| 71 |
+
ENABLE_SENTIMENT: true,
|
| 72 |
+
ENABLE_NEWS_SUMMARY: true,
|
| 73 |
+
ENABLE_PRICE_PREDICTION: false, // Currently disabled
|
| 74 |
+
ENABLE_PATTERN_DETECTION: false, // Currently disabled
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
+
// ═══════════════════════════════════════════════════════════════
|
| 78 |
+
// Notification Settings
|
| 79 |
+
// ═══════════════════════════════════════════════════════════════
|
| 80 |
+
|
| 81 |
+
NOTIFICATIONS: {
|
| 82 |
+
ENABLE: true,
|
| 83 |
+
SHOW_PRICE_ALERTS: true,
|
| 84 |
+
SHOW_NEWS_ALERTS: true,
|
| 85 |
+
AUTO_DISMISS_TIME: 5000, // 5 seconds
|
| 86 |
+
},
|
| 87 |
+
|
| 88 |
+
// ═══════════════════════════════════════════════════════════════
|
| 89 |
+
// UI Settings
|
| 90 |
+
// ═══════════════════════════════════════════════════════════════
|
| 91 |
|
| 92 |
+
UI: {
|
| 93 |
+
DEFAULT_THEME: 'dark', // 'dark' or 'light'
|
| 94 |
+
ENABLE_ANIMATIONS: true,
|
| 95 |
+
ENABLE_SOUNDS: false,
|
| 96 |
+
LANGUAGE: 'en', // 'en' or 'fa'
|
| 97 |
+
RTL: false,
|
| 98 |
+
},
|
| 99 |
+
|
| 100 |
+
// ═══════════════════════════════════════════════════════════════
|
| 101 |
+
// Debug Settings
|
| 102 |
+
// ═══════════════════════════════════════════════════════════════
|
| 103 |
+
|
| 104 |
+
DEBUG: {
|
| 105 |
+
ENABLE_CONSOLE_LOGS: true,
|
| 106 |
+
ENABLE_PERFORMANCE_MONITORING: true,
|
| 107 |
+
SHOW_API_REQUESTS: true,
|
| 108 |
+
SHOW_WS_MESSAGES: false,
|
| 109 |
+
},
|
| 110 |
+
|
| 111 |
+
// ═══════════════════════════════════════════════════════════════
|
| 112 |
+
// Default Filters and Sorting
|
| 113 |
+
// ═══════════════════════════════════════════════════════════════
|
| 114 |
+
|
| 115 |
+
FILTERS: {
|
| 116 |
+
DEFAULT_MARKET_FILTER: 'all', // 'all', 'gainers', 'losers', 'trending'
|
| 117 |
+
DEFAULT_NEWS_FILTER: 'all', // 'all', 'bitcoin', 'ethereum', 'defi', 'nft'
|
| 118 |
+
DEFAULT_SORT: 'market_cap', // 'market_cap', 'volume', 'price', 'change'
|
| 119 |
+
SORT_ORDER: 'desc', // 'asc' or 'desc'
|
| 120 |
+
},
|
| 121 |
+
|
| 122 |
+
// ═══════════════════════════════════════════════════════════════
|
| 123 |
+
// HuggingFace Configuration
|
| 124 |
+
// ═══════════════════════════════════════════════════════════════
|
| 125 |
+
|
| 126 |
+
HF_TOKEN: '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',
|
| 127 |
+
HF_API_BASE: 'https://api-inference.huggingface.co/models',
|
| 128 |
+
|
| 129 |
+
// ═══════════════════════════════════════════════════════════════
|
| 130 |
+
// API Endpoints (Optional - if your backend differs)
|
| 131 |
+
// ═══════════════════════════════════════════════════════════════
|
| 132 |
+
|
| 133 |
+
ENDPOINTS: {
|
| 134 |
+
HEALTH: '/api/health',
|
| 135 |
+
MARKET: '/api/market/stats',
|
| 136 |
+
MARKET_PRICES: '/api/market/prices',
|
| 137 |
+
COINS_TOP: '/api/coins/top',
|
| 138 |
+
COIN_DETAILS: '/api/coins',
|
| 139 |
+
TRENDING: '/api/trending',
|
| 140 |
+
SENTIMENT: '/api/sentiment',
|
| 141 |
+
SENTIMENT_ANALYZE: '/api/sentiment/analyze',
|
| 142 |
+
NEWS: '/api/news/latest',
|
| 143 |
+
NEWS_SUMMARIZE: '/api/news/summarize',
|
| 144 |
+
STATS: '/api/stats',
|
| 145 |
+
PROVIDERS: '/api/providers',
|
| 146 |
+
PROVIDER_STATUS: '/api/providers/status',
|
| 147 |
+
CHART_HISTORY: '/api/charts/price',
|
| 148 |
+
CHART_ANALYZE: '/api/charts/analyze',
|
| 149 |
+
OHLCV: '/api/ohlcv',
|
| 150 |
+
QUERY: '/api/query',
|
| 151 |
+
DATASETS: '/api/datasets/list',
|
| 152 |
+
MODELS: '/api/models/list',
|
| 153 |
+
HF_HEALTH: '/api/hf/health',
|
| 154 |
+
HF_REGISTRY: '/api/hf/registry',
|
| 155 |
+
SYSTEM_STATUS: '/api/system/status',
|
| 156 |
+
SYSTEM_CONFIG: '/api/system/config',
|
| 157 |
+
CATEGORIES: '/api/categories',
|
| 158 |
+
RATE_LIMITS: '/api/rate-limits',
|
| 159 |
+
LOGS: '/api/logs',
|
| 160 |
+
ALERTS: '/api/alerts',
|
| 161 |
+
},
|
| 162 |
+
|
| 163 |
+
// ═══════════════════════════════════════════════════════════════
|
| 164 |
+
// WebSocket Events
|
| 165 |
+
// ═══════════════════════════════════════════════════════════════
|
| 166 |
+
|
| 167 |
+
WS_EVENTS: {
|
| 168 |
+
MARKET_UPDATE: 'market_update',
|
| 169 |
+
SENTIMENT_UPDATE: 'sentiment_update',
|
| 170 |
+
NEWS_UPDATE: 'news_update',
|
| 171 |
+
STATS_UPDATE: 'stats_update',
|
| 172 |
+
PRICE_UPDATE: 'price_update',
|
| 173 |
+
API_UPDATE: 'api_update',
|
| 174 |
+
STATUS_UPDATE: 'status_update',
|
| 175 |
+
SCHEDULE_UPDATE: 'schedule_update',
|
| 176 |
+
CONNECTED: 'connected',
|
| 177 |
+
DISCONNECTED: 'disconnected',
|
| 178 |
+
},
|
| 179 |
+
|
| 180 |
+
// ═══════════════════════════════════════════════════════════════
|
| 181 |
+
// Display Formats
|
| 182 |
+
// ═══════════════════════════════════════════════════════════════
|
| 183 |
+
|
| 184 |
+
FORMATS: {
|
| 185 |
+
CURRENCY: {
|
| 186 |
+
LOCALE: 'en-US',
|
| 187 |
+
STYLE: 'currency',
|
| 188 |
+
CURRENCY: 'USD',
|
| 189 |
},
|
| 190 |
+
DATE: {
|
| 191 |
+
LOCALE: 'en-US',
|
| 192 |
+
OPTIONS: {
|
| 193 |
+
year: 'numeric',
|
| 194 |
+
month: 'long',
|
| 195 |
+
day: 'numeric',
|
| 196 |
+
hour: '2-digit',
|
| 197 |
+
minute: '2-digit',
|
| 198 |
+
},
|
| 199 |
+
},
|
| 200 |
+
},
|
| 201 |
+
|
| 202 |
+
// ═══════════════════════════════════════════════════════════════
|
| 203 |
+
// Rate Limiting
|
| 204 |
+
// ═══════════════════════════════════════════════════════════════
|
| 205 |
+
|
| 206 |
+
RATE_LIMITS: {
|
| 207 |
+
API_REQUESTS_PER_MINUTE: 60,
|
| 208 |
+
SEARCH_DEBOUNCE_MS: 300,
|
| 209 |
+
},
|
| 210 |
|
| 211 |
+
// ═══════════════════════════════════════════════════════════════
|
| 212 |
+
// Storage Settings
|
| 213 |
+
// ═══════════════════════════════════════════════════════════════
|
| 214 |
+
|
| 215 |
+
STORAGE: {
|
| 216 |
+
USE_LOCAL_STORAGE: true,
|
| 217 |
+
SAVE_PREFERENCES: true,
|
| 218 |
+
STORAGE_PREFIX: 'hts_dashboard_',
|
| 219 |
+
},
|
| 220 |
+
};
|
| 221 |
+
|
| 222 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 223 |
+
// Predefined Profiles
|
| 224 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 225 |
+
|
| 226 |
+
window.DASHBOARD_PROFILES = {
|
| 227 |
+
|
| 228 |
+
// High Performance Profile
|
| 229 |
+
HIGH_PERFORMANCE: {
|
| 230 |
+
UPDATE_INTERVAL: 15000, // Faster updates
|
| 231 |
+
CACHE_TTL: 30000, // Shorter cache
|
| 232 |
+
ENABLE_ANIMATIONS: false, // No animations
|
| 233 |
+
MAX_COINS_DISPLAY: 50,
|
| 234 |
+
},
|
| 235 |
+
|
| 236 |
+
// Data Saver Profile
|
| 237 |
+
DATA_SAVER: {
|
| 238 |
+
UPDATE_INTERVAL: 60000, // Less frequent updates
|
| 239 |
+
CACHE_TTL: 300000, // Longer cache (5 minutes)
|
| 240 |
+
MAX_COINS_DISPLAY: 10,
|
| 241 |
+
MAX_NEWS_DISPLAY: 10,
|
| 242 |
+
},
|
| 243 |
+
|
| 244 |
+
// Presentation Profile
|
| 245 |
+
PRESENTATION: {
|
| 246 |
+
ENABLE_ANIMATIONS: true,
|
| 247 |
+
UPDATE_INTERVAL: 20000,
|
| 248 |
+
SHOW_API_REQUESTS: false,
|
| 249 |
+
ENABLE_CONSOLE_LOGS: false,
|
| 250 |
+
},
|
| 251 |
+
|
| 252 |
+
// Development Profile
|
| 253 |
+
DEVELOPMENT: {
|
| 254 |
+
DEBUG: {
|
| 255 |
+
ENABLE_CONSOLE_LOGS: true,
|
| 256 |
+
ENABLE_PERFORMANCE_MONITORING: true,
|
| 257 |
+
SHOW_API_REQUESTS: true,
|
| 258 |
+
SHOW_WS_MESSAGES: true,
|
| 259 |
},
|
| 260 |
+
UPDATE_INTERVAL: 10000,
|
| 261 |
+
},
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 265 |
+
// Helper Function to Change Profile
|
| 266 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 267 |
+
|
| 268 |
+
window.applyDashboardProfile = function (profileName) {
|
| 269 |
+
if (window.DASHBOARD_PROFILES[profileName]) {
|
| 270 |
+
const profile = window.DASHBOARD_PROFILES[profileName];
|
| 271 |
+
Object.assign(window.DASHBOARD_CONFIG, profile);
|
| 272 |
+
console.log(`✅ Profile "${profileName}" applied`);
|
| 273 |
+
|
| 274 |
+
// Reload application with new settings
|
| 275 |
+
if (window.app) {
|
| 276 |
+
window.app.destroy();
|
| 277 |
+
window.app = new DashboardApp();
|
| 278 |
+
window.app.init();
|
| 279 |
+
}
|
| 280 |
+
} else {
|
| 281 |
+
console.error(`❌ Profile "${profileName}" not found`);
|
| 282 |
+
}
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 286 |
+
// Helper Function to Change Backend URL
|
| 287 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 288 |
+
|
| 289 |
+
window.changeBackendURL = function (httpUrl, wsUrl) {
|
| 290 |
+
window.DASHBOARD_CONFIG.BACKEND_URL = httpUrl;
|
| 291 |
+
window.DASHBOARD_CONFIG.WS_URL = wsUrl || httpUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
|
| 292 |
+
|
| 293 |
+
console.log('✅ Backend URL changed:');
|
| 294 |
+
console.log(' HTTP:', window.DASHBOARD_CONFIG.BACKEND_URL);
|
| 295 |
+
console.log(' WS:', window.DASHBOARD_CONFIG.WS_URL);
|
| 296 |
+
|
| 297 |
+
// Reload application
|
| 298 |
+
if (window.app) {
|
| 299 |
+
window.app.destroy();
|
| 300 |
+
window.app = new DashboardApp();
|
| 301 |
+
window.app.init();
|
| 302 |
+
}
|
| 303 |
+
};
|
| 304 |
|
| 305 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 306 |
+
// Save Settings to LocalStorage
|
| 307 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 308 |
+
|
| 309 |
+
window.saveConfig = function () {
|
| 310 |
+
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 311 |
+
try {
|
| 312 |
+
const configString = JSON.stringify(window.DASHBOARD_CONFIG);
|
| 313 |
+
localStorage.setItem(
|
| 314 |
+
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config',
|
| 315 |
+
configString
|
| 316 |
+
);
|
| 317 |
+
console.log('✅ Settings saved');
|
| 318 |
+
} catch (error) {
|
| 319 |
+
console.error('❌ Error saving settings:', error);
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 325 |
+
// Load Settings from LocalStorage
|
| 326 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 327 |
+
|
| 328 |
+
window.loadConfig = function () {
|
| 329 |
+
if (window.DASHBOARD_CONFIG.STORAGE.USE_LOCAL_STORAGE) {
|
| 330 |
+
try {
|
| 331 |
+
const configString = localStorage.getItem(
|
| 332 |
+
window.DASHBOARD_CONFIG.STORAGE.STORAGE_PREFIX + 'config'
|
| 333 |
+
);
|
| 334 |
+
if (configString) {
|
| 335 |
+
const savedConfig = JSON.parse(configString);
|
| 336 |
+
Object.assign(window.DASHBOARD_CONFIG, savedConfig);
|
| 337 |
+
console.log('✅ Settings loaded');
|
| 338 |
}
|
| 339 |
+
} catch (error) {
|
| 340 |
+
console.error('❌ Error loading settings:', error);
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
};
|
| 344 |
|
| 345 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 346 |
+
// Auto-load Settings on Page Load
|
| 347 |
+
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
+
if (document.readyState === 'loading') {
|
| 350 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 351 |
+
window.loadConfig();
|
| 352 |
+
});
|
| 353 |
+
} else {
|
| 354 |
+
window.loadConfig();
|
| 355 |
}
|
| 356 |
|
| 357 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 358 |
+
// Console Usage Guide
|
| 359 |
+
// ═══════════════════════════════════════════════════════════════════
|
| 360 |
+
|
| 361 |
+
console.log(`
|
| 362 |
+
╔═══════════════════════════════════════════════════════════════╗
|
| 363 |
+
║ HTS CRYPTO DASHBOARD - CONFIGURATION ║
|
| 364 |
+
╚═══════════════════════════════════════════════════════════════╝
|
| 365 |
+
|
| 366 |
+
📋 Available Commands:
|
| 367 |
+
|
| 368 |
+
1. Change Profile:
|
| 369 |
+
applyDashboardProfile('HIGH_PERFORMANCE')
|
| 370 |
+
applyDashboardProfile('DATA_SAVER')
|
| 371 |
+
applyDashboardProfile('PRESENTATION')
|
| 372 |
+
applyDashboardProfile('DEVELOPMENT')
|
| 373 |
+
|
| 374 |
+
2. Change Backend:
|
| 375 |
+
changeBackendURL('https://your-backend.com')
|
| 376 |
+
|
| 377 |
+
3. Save/Load Settings:
|
| 378 |
+
saveConfig()
|
| 379 |
+
loadConfig()
|
| 380 |
+
|
| 381 |
+
4. View Current Settings:
|
| 382 |
+
console.log(DASHBOARD_CONFIG)
|
| 383 |
+
|
| 384 |
+
5. Manual Settings Change:
|
| 385 |
+
DASHBOARD_CONFIG.UPDATE_INTERVAL = 20000
|
| 386 |
+
saveConfig()
|
| 387 |
+
|
| 388 |
+
═══════════════════════════════════════════════════════════════════
|
| 389 |
+
`);
|
config.py
CHANGED
|
@@ -13,6 +13,13 @@ from pathlib import Path
|
|
| 13 |
from typing import Dict, Any, List, Optional
|
| 14 |
from dataclasses import dataclass
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# ==================== DIRECTORIES ====================
|
| 17 |
BASE_DIR = Path(__file__).parent
|
| 18 |
DATA_DIR = BASE_DIR / "data"
|
|
@@ -86,6 +93,9 @@ def get_settings() -> Settings:
|
|
| 86 |
raw_token = os.environ.get("HF_TOKEN")
|
| 87 |
encoded_token = os.environ.get("HF_TOKEN_ENCODED")
|
| 88 |
decoded_token = raw_token or _decode_token(encoded_token)
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
database_path = Path(os.environ.get("DATABASE_PATH", str(DB_DIR / "crypto_aggregator.db")))
|
| 91 |
|
|
|
|
| 13 |
from typing import Dict, Any, List, Optional
|
| 14 |
from dataclasses import dataclass
|
| 15 |
|
| 16 |
+
# Load .env file if python-dotenv is available
|
| 17 |
+
try:
|
| 18 |
+
from dotenv import load_dotenv
|
| 19 |
+
load_dotenv()
|
| 20 |
+
except ImportError:
|
| 21 |
+
pass # python-dotenv not installed, skip loading .env
|
| 22 |
+
|
| 23 |
# ==================== DIRECTORIES ====================
|
| 24 |
BASE_DIR = Path(__file__).parent
|
| 25 |
DATA_DIR = BASE_DIR / "data"
|
|
|
|
| 93 |
raw_token = os.environ.get("HF_TOKEN")
|
| 94 |
encoded_token = os.environ.get("HF_TOKEN_ENCODED")
|
| 95 |
decoded_token = raw_token or _decode_token(encoded_token)
|
| 96 |
+
# Default token if none provided
|
| 97 |
+
if not decoded_token:
|
| 98 |
+
decoded_token = "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"
|
| 99 |
|
| 100 |
database_path = Path(os.environ.get("DATABASE_PATH", str(DB_DIR / "crypto_aggregator.db")))
|
| 101 |
|
crypto_resources_unified_2025-11-11.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
enhanced_server.py
CHANGED
|
@@ -199,9 +199,9 @@ from fastapi.responses import HTMLResponse, FileResponse
|
|
| 199 |
|
| 200 |
@app.get("/", response_class=HTMLResponse)
|
| 201 |
async def root():
|
| 202 |
-
"""Serve main dashboard"""
|
| 203 |
-
if os.path.exists("
|
| 204 |
-
return FileResponse("
|
| 205 |
else:
|
| 206 |
return HTMLResponse("""
|
| 207 |
<html>
|
|
|
|
| 199 |
|
| 200 |
@app.get("/", response_class=HTMLResponse)
|
| 201 |
async def root():
|
| 202 |
+
"""Serve main admin dashboard"""
|
| 203 |
+
if os.path.exists("admin.html"):
|
| 204 |
+
return FileResponse("admin.html")
|
| 205 |
else:
|
| 206 |
return HTMLResponse("""
|
| 207 |
<html>
|
hf_unified_server.py
CHANGED
|
@@ -2,16 +2,38 @@
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from datetime import datetime, timedelta
|
| 6 |
-
from fastapi import Body, FastAPI, HTTPException, Query
|
| 7 |
from fastapi.middleware.cors import CORSMiddleware
|
| 8 |
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
| 10 |
from typing import Any, Dict, List, Optional, Union
|
|
|
|
| 11 |
import logging
|
| 12 |
import random
|
| 13 |
import json
|
| 14 |
from pathlib import Path
|
|
|
|
| 15 |
|
| 16 |
from ai_models import (
|
| 17 |
analyze_chart_points,
|
|
@@ -21,6 +43,7 @@ from ai_models import (
|
|
| 21 |
initialize_models,
|
| 22 |
registry_status,
|
| 23 |
)
|
|
|
|
| 24 |
from collectors.aggregator import (
|
| 25 |
CollectorError,
|
| 26 |
MarketDataCollector,
|
|
@@ -60,6 +83,7 @@ provider_collector = ProviderStatusCollector()
|
|
| 60 |
# Load providers config
|
| 61 |
WORKSPACE_ROOT = Path(__file__).parent
|
| 62 |
PROVIDERS_CONFIG_PATH = settings.providers_config_path
|
|
|
|
| 63 |
|
| 64 |
def load_providers_config():
|
| 65 |
"""Load providers from providers_config_extended.json"""
|
|
@@ -68,28 +92,67 @@ def load_providers_config():
|
|
| 68 |
with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
| 69 |
config = json.load(f)
|
| 70 |
providers = config.get('providers', {})
|
| 71 |
-
logger.info(f"
|
| 72 |
return providers
|
| 73 |
else:
|
| 74 |
-
logger.warning(f"
|
| 75 |
return {}
|
| 76 |
except Exception as e:
|
| 77 |
-
logger.error(f"
|
| 78 |
return {}
|
| 79 |
|
| 80 |
# Load providers at startup
|
| 81 |
PROVIDERS_CONFIG = load_providers_config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# Mount static files (CSS, JS)
|
| 84 |
try:
|
| 85 |
static_path = WORKSPACE_ROOT / "static"
|
| 86 |
if static_path.exists():
|
| 87 |
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
| 88 |
-
logger.info(f"
|
| 89 |
else:
|
| 90 |
-
logger.warning(f"
|
| 91 |
except Exception as e:
|
| 92 |
-
logger.error(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
# ============================================================================
|
| 95 |
# Helper utilities & Data Fetching Functions
|
|
@@ -131,7 +194,11 @@ def _format_price_record(record: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 131 |
|
| 132 |
|
| 133 |
async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", limit: int = 100):
|
| 134 |
-
"""Fetch OHLCV data from Binance via the shared collector.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
try:
|
| 137 |
candles = await market_collector.get_ohlcv(symbol, interval, limit)
|
|
@@ -151,6 +218,7 @@ async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", lim
|
|
| 151 |
async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int = 10):
|
| 152 |
"""Fetch price snapshots using the shared market collector."""
|
| 153 |
|
|
|
|
| 154 |
try:
|
| 155 |
if symbols:
|
| 156 |
tasks = [market_collector.get_coin_details(_normalize_asset_symbol(sym)) for sym in symbols]
|
|
@@ -160,13 +228,17 @@ async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int
|
|
| 160 |
if isinstance(result, Exception):
|
| 161 |
continue
|
| 162 |
coins.append(_format_price_record(result))
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 167 |
except CollectorError as exc:
|
| 168 |
logger.error("Error fetching aggregated prices: %s", exc)
|
| 169 |
-
return
|
|
|
|
| 170 |
|
| 171 |
|
| 172 |
async def fetch_binance_ticker(symbol: str):
|
|
@@ -176,22 +248,29 @@ async def fetch_binance_ticker(symbol: str):
|
|
| 176 |
coin = await market_collector.get_coin_details(_normalize_asset_symbol(symbol))
|
| 177 |
except CollectorError as exc:
|
| 178 |
logger.error("Unable to load ticker for %s: %s", symbol, exc)
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
price = coin.get("price")
|
| 182 |
-
change_pct = coin.get("change_24h") or 0.0
|
| 183 |
-
change_abs = price * change_pct / 100 if price is not None and change_pct is not None else None
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
"
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
# ============================================================================
|
|
@@ -300,6 +379,51 @@ async def get_providers():
|
|
| 300 |
}
|
| 301 |
|
| 302 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
# ============================================================================
|
| 304 |
# OHLCV Data Endpoint
|
| 305 |
# ============================================================================
|
|
@@ -364,7 +488,7 @@ async def get_top_prices(limit: int = Query(10, ge=1, le=100, description="Numbe
|
|
| 364 |
return {"data": cached_data, "source": "cache"}
|
| 365 |
|
| 366 |
# Fetch from CoinGecko
|
| 367 |
-
prices = await fetch_coingecko_prices(limit=limit)
|
| 368 |
|
| 369 |
if prices:
|
| 370 |
# Update cache
|
|
@@ -373,7 +497,7 @@ async def get_top_prices(limit: int = Query(10, ge=1, le=100, description="Numbe
|
|
| 373 |
return {
|
| 374 |
"count": len(prices),
|
| 375 |
"data": prices,
|
| 376 |
-
"source":
|
| 377 |
"timestamp": datetime.now().isoformat()
|
| 378 |
}
|
| 379 |
else:
|
|
@@ -392,23 +516,23 @@ async def get_single_price(symbol: str):
|
|
| 392 |
try:
|
| 393 |
# Try Binance first for common pairs
|
| 394 |
binance_symbol = f"{symbol.upper()}USDT"
|
| 395 |
-
ticker = await fetch_binance_ticker(binance_symbol)
|
| 396 |
|
| 397 |
if ticker:
|
| 398 |
return {
|
| 399 |
"symbol": symbol.upper(),
|
| 400 |
"price": ticker,
|
| 401 |
-
"source":
|
| 402 |
"timestamp": datetime.now().isoformat()
|
| 403 |
}
|
| 404 |
|
| 405 |
# Fallback to CoinGecko
|
| 406 |
-
prices = await fetch_coingecko_prices([symbol])
|
| 407 |
if prices:
|
| 408 |
return {
|
| 409 |
"symbol": symbol.upper(),
|
| 410 |
"price": prices[0],
|
| 411 |
-
"source":
|
| 412 |
"timestamp": datetime.now().isoformat()
|
| 413 |
}
|
| 414 |
|
|
@@ -426,14 +550,51 @@ async def get_market_overview():
|
|
| 426 |
"""Get comprehensive market overview"""
|
| 427 |
try:
|
| 428 |
# Fetch top 20 coins
|
| 429 |
-
prices = await fetch_coingecko_prices(limit=20)
|
| 430 |
|
| 431 |
if not prices:
|
| 432 |
raise HTTPException(status_code=503, detail="Unable to fetch market data")
|
| 433 |
|
| 434 |
# Calculate market stats
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
# Sort by 24h change
|
| 439 |
gainers = sorted(
|
|
@@ -454,7 +615,8 @@ async def get_market_overview():
|
|
| 454 |
"top_gainers": gainers,
|
| 455 |
"top_losers": losers,
|
| 456 |
"top_by_volume": sorted(prices, key=lambda x: x.get("total_volume", 0) or 0, reverse=True)[:5],
|
| 457 |
-
"timestamp": datetime.now().isoformat()
|
|
|
|
| 458 |
}
|
| 459 |
|
| 460 |
except HTTPException:
|
|
@@ -464,6 +626,59 @@ async def get_market_overview():
|
|
| 464 |
raise HTTPException(status_code=500, detail=str(e))
|
| 465 |
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
@app.get("/api/market/prices")
|
| 468 |
async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="Comma-separated symbols")):
|
| 469 |
"""Get prices for multiple cryptocurrencies"""
|
|
@@ -472,22 +687,29 @@ async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="C
|
|
| 472 |
|
| 473 |
# Fetch prices
|
| 474 |
prices_data = []
|
|
|
|
| 475 |
for symbol in symbol_list:
|
| 476 |
try:
|
| 477 |
-
ticker = await fetch_binance_ticker(f"{symbol}USDT")
|
| 478 |
if ticker:
|
| 479 |
prices_data.append(ticker)
|
|
|
|
|
|
|
| 480 |
except:
|
| 481 |
continue
|
|
|
|
|
|
|
|
|
|
| 482 |
|
|
|
|
| 483 |
if not prices_data:
|
| 484 |
-
|
| 485 |
-
prices_data = await fetch_coingecko_prices(symbol_list)
|
| 486 |
|
| 487 |
return {
|
| 488 |
"symbols": symbol_list,
|
| 489 |
"count": len(prices_data),
|
| 490 |
"data": prices_data,
|
|
|
|
| 491 |
"timestamp": datetime.now().isoformat()
|
| 492 |
}
|
| 493 |
|
|
@@ -614,7 +836,7 @@ async def get_scoring_snapshot(symbol: str = Query("BTCUSDT", description="Tradi
|
|
| 614 |
"""Get comprehensive scoring snapshot"""
|
| 615 |
try:
|
| 616 |
# Fetch data
|
| 617 |
-
ticker = await fetch_binance_ticker(symbol)
|
| 618 |
ohlcv = await fetch_binance_ohlcv(symbol, "1h", 100)
|
| 619 |
|
| 620 |
if not ticker or not ohlcv:
|
|
@@ -674,14 +896,25 @@ async def get_all_signals():
|
|
| 674 |
|
| 675 |
@app.get("/api/sentiment")
|
| 676 |
async def get_sentiment():
|
| 677 |
-
"""Get market sentiment data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
try:
|
| 679 |
news = await news_collector.get_latest_news(limit=5)
|
| 680 |
except CollectorError as exc:
|
| 681 |
-
logger.warning("Sentiment
|
| 682 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
-
text = " ".join(item.get("title", "") for item in news).strip() or "Crypto market update"
|
| 685 |
analysis = analyze_market_text(text)
|
| 686 |
score = analysis.get("signals", {}).get("crypto", {}).get("score", 0.0)
|
| 687 |
normalized_value = int((score + 1) * 50)
|
|
@@ -808,7 +1041,9 @@ async def get_alerts():
|
|
| 808 |
@app.get("/api/hf/health")
|
| 809 |
async def hf_health():
|
| 810 |
"""HuggingFace integration health"""
|
|
|
|
| 811 |
status = registry_status()
|
|
|
|
| 812 |
status["timestamp"] = datetime.utcnow().isoformat()
|
| 813 |
return status
|
| 814 |
|
|
@@ -816,8 +1051,9 @@ async def hf_health():
|
|
| 816 |
@app.post("/api/hf/refresh")
|
| 817 |
async def hf_refresh():
|
| 818 |
"""Refresh HuggingFace data"""
|
|
|
|
| 819 |
result = initialize_models()
|
| 820 |
-
return {"status": "ok" if result.get("
|
| 821 |
|
| 822 |
|
| 823 |
@app.get("/api/hf/registry")
|
|
@@ -827,6 +1063,83 @@ async def hf_registry(kind: str = "models"):
|
|
| 827 |
return {"kind": kind, "items": info.get("model_names", info)}
|
| 828 |
|
| 829 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dict[str, Any]:
|
| 831 |
if isinstance(payload, list):
|
| 832 |
return {"texts": payload, "mode": "auto"}
|
|
@@ -845,6 +1158,15 @@ def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dic
|
|
| 845 |
@app.post("/api/hf/sentiment")
|
| 846 |
async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
|
| 847 |
"""Run sentiment analysis using shared AI helpers."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 848 |
|
| 849 |
try:
|
| 850 |
resolved = _resolve_sentiment_payload(payload)
|
|
@@ -868,16 +1190,173 @@ async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
|
|
| 868 |
return {"mode": mode, "results": results, "timestamp": datetime.utcnow().isoformat()}
|
| 869 |
|
| 870 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 871 |
# ============================================================================
|
| 872 |
# HTML Routes - Serve UI files
|
| 873 |
# ============================================================================
|
| 874 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 875 |
@app.get("/", response_class=HTMLResponse)
|
| 876 |
async def root():
|
| 877 |
-
"""Serve main
|
| 878 |
-
|
| 879 |
-
if
|
| 880 |
-
return FileResponse(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
return HTMLResponse("<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>")
|
| 882 |
|
| 883 |
@app.get("/index.html", response_class=HTMLResponse)
|
|
@@ -898,12 +1377,26 @@ async def dashboard_alt():
|
|
| 898 |
@app.get("/admin.html", response_class=HTMLResponse)
|
| 899 |
async def admin():
|
| 900 |
"""Serve admin panel"""
|
| 901 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
|
| 903 |
@app.get("/admin", response_class=HTMLResponse)
|
| 904 |
async def admin_alt():
|
| 905 |
"""Alternative route for admin"""
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
|
| 908 |
@app.get("/hf_console.html", response_class=HTMLResponse)
|
| 909 |
async def hf_console():
|
|
@@ -944,43 +1437,281 @@ async def serve_html(filename: str):
|
|
| 944 |
# Startup Event
|
| 945 |
# ============================================================================
|
| 946 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
@app.on_event("startup")
|
| 948 |
async def startup_event():
|
| 949 |
-
"""Initialize on startup"""
|
| 950 |
-
# Initialize AI models
|
| 951 |
-
from ai_models import initialize_models
|
| 952 |
-
models_init = initialize_models()
|
| 953 |
-
logger.info(f"✓ AI Models initialized: {models_init}")
|
| 954 |
-
|
| 955 |
-
# Initialize HF Registry
|
| 956 |
-
from backend.services.hf_registry import REGISTRY
|
| 957 |
-
registry_result = await REGISTRY.refresh()
|
| 958 |
-
logger.info(f"✓ HF Registry initialized: {registry_result}")
|
| 959 |
-
|
| 960 |
logger.info("=" * 70)
|
| 961 |
-
logger.info("
|
| 962 |
logger.info("=" * 70)
|
| 963 |
-
logger.info("
|
| 964 |
-
logger.info("
|
| 965 |
-
logger.info("
|
| 966 |
-
logger.info(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 967 |
|
| 968 |
# Show loaded HuggingFace Space providers
|
| 969 |
hf_providers = [p for p in PROVIDERS_CONFIG.keys() if 'huggingface_space' in p]
|
| 970 |
if hf_providers:
|
| 971 |
-
logger.info(f"
|
| 972 |
|
| 973 |
-
logger.info("
|
| 974 |
|
| 975 |
# Check HTML files
|
| 976 |
html_files = ["index.html", "dashboard.html", "admin.html", "hf_console.html"]
|
| 977 |
available_html = [f for f in html_files if (WORKSPACE_ROOT / f).exists()]
|
| 978 |
-
logger.info(f"
|
|
|
|
| 979 |
|
| 980 |
logger.info("=" * 70)
|
| 981 |
-
logger.info("
|
| 982 |
-
logger.info("
|
| 983 |
-
logger.info("
|
| 984 |
logger.info("=" * 70)
|
| 985 |
|
| 986 |
|
|
@@ -990,14 +1721,31 @@ async def startup_event():
|
|
| 990 |
|
| 991 |
if __name__ == "__main__":
|
| 992 |
import uvicorn
|
|
|
|
|
|
|
| 993 |
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1001 |
|
| 1002 |
uvicorn.run(
|
| 1003 |
app,
|
|
@@ -1026,11 +1774,21 @@ class ConnectionManager:
|
|
| 1026 |
logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
|
| 1027 |
|
| 1028 |
async def broadcast(self, message: dict):
|
|
|
|
| 1029 |
for connection in list(self.active_connections):
|
| 1030 |
try:
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
|
| 1035 |
ws_manager = ConnectionManager()
|
| 1036 |
|
|
@@ -1056,14 +1814,21 @@ async def get_top_coins(limit: int = Query(default=10, ge=1, le=100)):
|
|
| 1056 |
result = []
|
| 1057 |
for coin in coins:
|
| 1058 |
result.append({
|
|
|
|
| 1059 |
"rank": coin.get("rank", 0),
|
| 1060 |
"symbol": coin.get("symbol", "").upper(),
|
| 1061 |
"name": coin.get("name", ""),
|
| 1062 |
"price": coin.get("price") or coin.get("current_price", 0),
|
|
|
|
| 1063 |
"price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
|
|
|
|
|
|
|
| 1064 |
"volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
|
|
|
|
| 1065 |
"market_cap": coin.get("market_cap", 0),
|
| 1066 |
"image": coin.get("image", ""),
|
|
|
|
|
|
|
| 1067 |
"last_updated": coin.get("last_updated", datetime.now().isoformat())
|
| 1068 |
})
|
| 1069 |
|
|
@@ -1111,14 +1876,26 @@ async def get_coin_detail(symbol: str):
|
|
| 1111 |
async def get_market_stats():
|
| 1112 |
"""Get global market statistics"""
|
| 1113 |
try:
|
| 1114 |
-
# Use existing endpoint
|
| 1115 |
overview = await get_market_overview()
|
| 1116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
stats = {
|
| 1118 |
-
"total_market_cap": overview.get("
|
| 1119 |
-
"total_volume_24h": overview.get("
|
| 1120 |
-
"btc_dominance": overview.get("btc_dominance", 0),
|
| 1121 |
-
"eth_dominance":
|
| 1122 |
"active_cryptocurrencies": 10000, # Approximate
|
| 1123 |
"markets": 500, # Approximate
|
| 1124 |
"market_cap_change_24h": 0.0,
|
|
@@ -1175,6 +1952,12 @@ async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
|
|
| 1175 |
return {"success": True, "news": [], "count": 0, "timestamp": datetime.now().isoformat()}
|
| 1176 |
|
| 1177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
@app.post("/api/news/summarize")
|
| 1179 |
async def summarize_news(item: Dict[str, Any] = Body(...)):
|
| 1180 |
"""Summarize a news article"""
|
|
@@ -1203,30 +1986,112 @@ async def summarize_news(item: Dict[str, Any] = Body(...)):
|
|
| 1203 |
async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
|
| 1204 |
"""Get price chart data"""
|
| 1205 |
try:
|
| 1206 |
-
#
|
| 1207 |
-
|
| 1208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
|
| 1210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1211 |
|
| 1212 |
chart_data = []
|
| 1213 |
for point in price_history:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
chart_data.append({
|
| 1215 |
-
"timestamp":
|
| 1216 |
-
"
|
| 1217 |
-
"date":
|
|
|
|
|
|
|
|
|
|
| 1218 |
})
|
| 1219 |
|
|
|
|
|
|
|
| 1220 |
return {
|
| 1221 |
"success": True,
|
| 1222 |
-
"symbol": symbol
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1230 |
|
| 1231 |
|
| 1232 |
@app.post("/api/charts/analyze")
|
|
@@ -1237,12 +2102,38 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
|
|
| 1237 |
timeframe = payload.get("timeframe", "7d")
|
| 1238 |
indicators = payload.get("indicators", [])
|
| 1239 |
|
| 1240 |
-
|
| 1241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
|
| 1243 |
# Analyze with AI
|
| 1244 |
from ai_models import analyze_chart_points
|
| 1245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1246 |
|
| 1247 |
return {
|
| 1248 |
"success": True,
|
|
@@ -1250,9 +2141,18 @@ async def analyze_chart(payload: Dict[str, Any] = Body(...)):
|
|
| 1250 |
"timeframe": timeframe,
|
| 1251 |
"analysis": analysis
|
| 1252 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1253 |
except Exception as e:
|
| 1254 |
-
logger.error(f"Error in /api/charts/analyze: {e}")
|
| 1255 |
-
return
|
|
|
|
|
|
|
|
|
|
| 1256 |
|
| 1257 |
|
| 1258 |
# ===== SENTIMENT ENDPOINTS =====
|
|
@@ -1344,7 +2244,18 @@ async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=
|
|
| 1344 |
# Attempt to load dataset
|
| 1345 |
try:
|
| 1346 |
from datasets import load_dataset
|
| 1347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
|
| 1349 |
sample = []
|
| 1350 |
for i, row in enumerate(dataset):
|
|
@@ -1429,6 +2340,11 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 1429 |
|
| 1430 |
try:
|
| 1431 |
while True:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1432 |
# Send market updates every 10 seconds
|
| 1433 |
try:
|
| 1434 |
# Get latest data
|
|
@@ -1451,16 +2367,42 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 1451 |
"timestamp": datetime.now().isoformat()
|
| 1452 |
}
|
| 1453 |
|
| 1454 |
-
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1458 |
except Exception as e:
|
| 1459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
|
| 1461 |
await asyncio.sleep(10)
|
| 1462 |
except WebSocketDisconnect:
|
| 1463 |
-
|
| 1464 |
except Exception as e:
|
| 1465 |
logger.error(f"WebSocket error: {e}")
|
| 1466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import asyncio
|
| 4 |
import time
|
| 5 |
+
import os
|
| 6 |
+
import sys
|
| 7 |
+
import io
|
| 8 |
+
|
| 9 |
+
# Fix encoding for Windows console (must be done before any print/logging)
|
| 10 |
+
if sys.platform == "win32":
|
| 11 |
+
try:
|
| 12 |
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
| 13 |
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
| 14 |
+
except Exception:
|
| 15 |
+
pass # If already wrapped, ignore
|
| 16 |
+
|
| 17 |
+
# Set environment variables to force PyTorch and avoid TensorFlow/Keras issues
|
| 18 |
+
os.environ.setdefault('TRANSFORMERS_NO_ADVISORY_WARNINGS', '1')
|
| 19 |
+
os.environ.setdefault('TRANSFORMERS_VERBOSITY', 'error')
|
| 20 |
+
os.environ.setdefault('TF_CPP_MIN_LOG_LEVEL', '3') # Suppress TensorFlow warnings
|
| 21 |
+
# Force PyTorch as default framework
|
| 22 |
+
os.environ.setdefault('TRANSFORMERS_FRAMEWORK', 'pt')
|
| 23 |
+
|
| 24 |
from datetime import datetime, timedelta
|
| 25 |
+
from fastapi import Body, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
| 26 |
from fastapi.middleware.cors import CORSMiddleware
|
| 27 |
from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
|
| 28 |
from fastapi.staticfiles import StaticFiles
|
| 29 |
+
from starlette.websockets import WebSocketState
|
| 30 |
from typing import Any, Dict, List, Optional, Union
|
| 31 |
+
from statistics import mean
|
| 32 |
import logging
|
| 33 |
import random
|
| 34 |
import json
|
| 35 |
from pathlib import Path
|
| 36 |
+
import httpx
|
| 37 |
|
| 38 |
from ai_models import (
|
| 39 |
analyze_chart_points,
|
|
|
|
| 43 |
initialize_models,
|
| 44 |
registry_status,
|
| 45 |
)
|
| 46 |
+
from backend.services.local_resource_service import LocalResourceService
|
| 47 |
from collectors.aggregator import (
|
| 48 |
CollectorError,
|
| 49 |
MarketDataCollector,
|
|
|
|
| 83 |
# Load providers config
|
| 84 |
WORKSPACE_ROOT = Path(__file__).parent
|
| 85 |
PROVIDERS_CONFIG_PATH = settings.providers_config_path
|
| 86 |
+
FALLBACK_RESOURCE_PATH = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
|
| 87 |
|
| 88 |
def load_providers_config():
|
| 89 |
"""Load providers from providers_config_extended.json"""
|
|
|
|
| 92 |
with open(PROVIDERS_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
| 93 |
config = json.load(f)
|
| 94 |
providers = config.get('providers', {})
|
| 95 |
+
logger.info(f"Loaded {len(providers)} providers from providers_config_extended.json")
|
| 96 |
return providers
|
| 97 |
else:
|
| 98 |
+
logger.warning(f"providers_config_extended.json not found at {PROVIDERS_CONFIG_PATH}")
|
| 99 |
return {}
|
| 100 |
except Exception as e:
|
| 101 |
+
logger.error(f"Error loading providers config: {e}")
|
| 102 |
return {}
|
| 103 |
|
| 104 |
# Load providers at startup
|
| 105 |
PROVIDERS_CONFIG = load_providers_config()
|
| 106 |
+
local_resource_service = LocalResourceService(FALLBACK_RESOURCE_PATH)
|
| 107 |
+
|
| 108 |
+
HF_SAMPLE_NEWS = [
|
| 109 |
+
{
|
| 110 |
+
"title": "Bitcoin holds key liquidity zone",
|
| 111 |
+
"source": "Fallback Ledger",
|
| 112 |
+
"sentiment": "positive",
|
| 113 |
+
"sentiment_score": 0.64,
|
| 114 |
+
"entities": ["BTC"],
|
| 115 |
+
"summary": "BTC consolidates near resistance with steady inflows",
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"title": "Ethereum staking demand remains resilient",
|
| 119 |
+
"source": "Fallback Ledger",
|
| 120 |
+
"sentiment": "neutral",
|
| 121 |
+
"sentiment_score": 0.12,
|
| 122 |
+
"entities": ["ETH"],
|
| 123 |
+
"summary": "Validator queue shortens as fees stabilize around L2 adoption",
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"title": "Solana ecosystem sees TVL uptick",
|
| 127 |
+
"source": "Fallback Ledger",
|
| 128 |
+
"sentiment": "positive",
|
| 129 |
+
"sentiment_score": 0.41,
|
| 130 |
+
"entities": ["SOL"],
|
| 131 |
+
"summary": "DeFi protocols move to Solana as mempool congestion drops",
|
| 132 |
+
},
|
| 133 |
+
]
|
| 134 |
|
| 135 |
# Mount static files (CSS, JS)
|
| 136 |
try:
|
| 137 |
static_path = WORKSPACE_ROOT / "static"
|
| 138 |
if static_path.exists():
|
| 139 |
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
|
| 140 |
+
logger.info(f"Static files mounted from {static_path}")
|
| 141 |
else:
|
| 142 |
+
logger.warning(f"Static directory not found: {static_path}")
|
| 143 |
except Exception as e:
|
| 144 |
+
logger.error(f"Error mounting static files: {e}")
|
| 145 |
+
|
| 146 |
+
# Mount api-resources for frontend access
|
| 147 |
+
try:
|
| 148 |
+
api_resources_path = WORKSPACE_ROOT / "api-resources"
|
| 149 |
+
if api_resources_path.exists():
|
| 150 |
+
app.mount("/api-resources", StaticFiles(directory=str(api_resources_path)), name="api-resources")
|
| 151 |
+
logger.info(f"API resources mounted from {api_resources_path}")
|
| 152 |
+
else:
|
| 153 |
+
logger.warning(f"API resources directory not found: {api_resources_path}")
|
| 154 |
+
except Exception as e:
|
| 155 |
+
logger.error(f"Error mounting API resources: {e}")
|
| 156 |
|
| 157 |
# ============================================================================
|
| 158 |
# Helper utilities & Data Fetching Functions
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
async def fetch_binance_ohlcv(symbol: str = "BTCUSDT", interval: str = "1h", limit: int = 100):
|
| 197 |
+
"""Fetch OHLCV data from Binance via the shared collector.
|
| 198 |
+
|
| 199 |
+
If live data cannot be retrieved, this helper returns an empty list instead of
|
| 200 |
+
using any local/mock fallback so that upstream endpoints can fail honestly.
|
| 201 |
+
"""
|
| 202 |
|
| 203 |
try:
|
| 204 |
candles = await market_collector.get_ohlcv(symbol, interval, limit)
|
|
|
|
| 218 |
async def fetch_coingecko_prices(symbols: Optional[List[str]] = None, limit: int = 10):
|
| 219 |
"""Fetch price snapshots using the shared market collector."""
|
| 220 |
|
| 221 |
+
source = "coingecko"
|
| 222 |
try:
|
| 223 |
if symbols:
|
| 224 |
tasks = [market_collector.get_coin_details(_normalize_asset_symbol(sym)) for sym in symbols]
|
|
|
|
| 228 |
if isinstance(result, Exception):
|
| 229 |
continue
|
| 230 |
coins.append(_format_price_record(result))
|
| 231 |
+
if coins:
|
| 232 |
+
return coins, source
|
| 233 |
+
else:
|
| 234 |
+
top = await market_collector.get_top_coins(limit=limit)
|
| 235 |
+
formatted = [_format_price_record(entry) for entry in top]
|
| 236 |
+
if formatted:
|
| 237 |
+
return formatted, source
|
| 238 |
except CollectorError as exc:
|
| 239 |
logger.error("Error fetching aggregated prices: %s", exc)
|
| 240 |
+
# No local/mock fallback – return an empty list so callers can signal real failure
|
| 241 |
+
return [], source
|
| 242 |
|
| 243 |
|
| 244 |
async def fetch_binance_ticker(symbol: str):
|
|
|
|
| 248 |
coin = await market_collector.get_coin_details(_normalize_asset_symbol(symbol))
|
| 249 |
except CollectorError as exc:
|
| 250 |
logger.error("Unable to load ticker for %s: %s", symbol, exc)
|
| 251 |
+
coin = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
+
if coin:
|
| 254 |
+
price = coin.get("price")
|
| 255 |
+
change_pct = coin.get("change_24h") or 0.0
|
| 256 |
+
change_abs = price * change_pct / 100 if price is not None and change_pct is not None else None
|
| 257 |
+
return {
|
| 258 |
+
"symbol": symbol.upper(),
|
| 259 |
+
"price": price,
|
| 260 |
+
"price_change_24h": change_abs,
|
| 261 |
+
"price_change_percent_24h": change_pct,
|
| 262 |
+
"high_24h": coin.get("high_24h"),
|
| 263 |
+
"low_24h": coin.get("low_24h"),
|
| 264 |
+
"volume_24h": coin.get("volume_24h"),
|
| 265 |
+
"quote_volume_24h": coin.get("volume_24h"),
|
| 266 |
+
}, "binance"
|
| 267 |
+
|
| 268 |
+
fallback_symbol = _normalize_asset_symbol(symbol)
|
| 269 |
+
fallback = local_resource_service.get_ticker_snapshot(fallback_symbol)
|
| 270 |
+
if fallback:
|
| 271 |
+
fallback["symbol"] = symbol.upper()
|
| 272 |
+
return fallback, "local-fallback"
|
| 273 |
+
return None, "binance"
|
| 274 |
|
| 275 |
|
| 276 |
# ============================================================================
|
|
|
|
| 379 |
}
|
| 380 |
|
| 381 |
|
| 382 |
+
@app.get("/api/providers/{provider_id}/health")
|
| 383 |
+
async def get_provider_health(provider_id: str):
|
| 384 |
+
"""Get health status for a specific provider."""
|
| 385 |
+
|
| 386 |
+
# Check if provider exists in config
|
| 387 |
+
provider_config = PROVIDERS_CONFIG.get(provider_id)
|
| 388 |
+
if not provider_config:
|
| 389 |
+
raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
|
| 390 |
+
|
| 391 |
+
try:
|
| 392 |
+
# Perform health check using the collector
|
| 393 |
+
async with httpx.AsyncClient(timeout=provider_collector.timeout, headers=provider_collector.headers) as client:
|
| 394 |
+
health_result = await provider_collector._check_provider(client, provider_id, provider_config)
|
| 395 |
+
|
| 396 |
+
# Add metadata from config
|
| 397 |
+
health_result.update({
|
| 398 |
+
"base_url": provider_config.get("base_url"),
|
| 399 |
+
"requires_auth": provider_config.get("requires_auth"),
|
| 400 |
+
"priority": provider_config.get("priority"),
|
| 401 |
+
"category": provider_config.get("category"),
|
| 402 |
+
"last_checked": datetime.utcnow().isoformat()
|
| 403 |
+
})
|
| 404 |
+
|
| 405 |
+
return health_result
|
| 406 |
+
except Exception as exc: # pragma: no cover - network heavy
|
| 407 |
+
logger.error("Error checking provider health for %s: %s", provider_id, exc)
|
| 408 |
+
raise HTTPException(status_code=503, detail=f"Health check failed: {str(exc)}")
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@app.get("/api/providers/config")
|
| 412 |
+
async def get_providers_config():
|
| 413 |
+
"""Get providers configuration in format expected by frontend."""
|
| 414 |
+
try:
|
| 415 |
+
return {
|
| 416 |
+
"success": True,
|
| 417 |
+
"providers": PROVIDERS_CONFIG,
|
| 418 |
+
"total": len(PROVIDERS_CONFIG),
|
| 419 |
+
"source": str(PROVIDERS_CONFIG_PATH),
|
| 420 |
+
"last_updated": datetime.utcnow().isoformat()
|
| 421 |
+
}
|
| 422 |
+
except Exception as exc:
|
| 423 |
+
logger.error("Error getting providers config: %s", exc)
|
| 424 |
+
raise HTTPException(status_code=500, detail=str(exc))
|
| 425 |
+
|
| 426 |
+
|
| 427 |
# ============================================================================
|
| 428 |
# OHLCV Data Endpoint
|
| 429 |
# ============================================================================
|
|
|
|
| 488 |
return {"data": cached_data, "source": "cache"}
|
| 489 |
|
| 490 |
# Fetch from CoinGecko
|
| 491 |
+
prices, source = await fetch_coingecko_prices(limit=limit)
|
| 492 |
|
| 493 |
if prices:
|
| 494 |
# Update cache
|
|
|
|
| 497 |
return {
|
| 498 |
"count": len(prices),
|
| 499 |
"data": prices,
|
| 500 |
+
"source": source,
|
| 501 |
"timestamp": datetime.now().isoformat()
|
| 502 |
}
|
| 503 |
else:
|
|
|
|
| 516 |
try:
|
| 517 |
# Try Binance first for common pairs
|
| 518 |
binance_symbol = f"{symbol.upper()}USDT"
|
| 519 |
+
ticker, ticker_source = await fetch_binance_ticker(binance_symbol)
|
| 520 |
|
| 521 |
if ticker:
|
| 522 |
return {
|
| 523 |
"symbol": symbol.upper(),
|
| 524 |
"price": ticker,
|
| 525 |
+
"source": ticker_source,
|
| 526 |
"timestamp": datetime.now().isoformat()
|
| 527 |
}
|
| 528 |
|
| 529 |
# Fallback to CoinGecko
|
| 530 |
+
prices, source = await fetch_coingecko_prices([symbol])
|
| 531 |
if prices:
|
| 532 |
return {
|
| 533 |
"symbol": symbol.upper(),
|
| 534 |
"price": prices[0],
|
| 535 |
+
"source": source,
|
| 536 |
"timestamp": datetime.now().isoformat()
|
| 537 |
}
|
| 538 |
|
|
|
|
| 550 |
"""Get comprehensive market overview"""
|
| 551 |
try:
|
| 552 |
# Fetch top 20 coins
|
| 553 |
+
prices, source = await fetch_coingecko_prices(limit=20)
|
| 554 |
|
| 555 |
if not prices:
|
| 556 |
raise HTTPException(status_code=503, detail="Unable to fetch market data")
|
| 557 |
|
| 558 |
# Calculate market stats
|
| 559 |
+
# Try multiple field names for market cap and volume
|
| 560 |
+
total_market_cap = 0
|
| 561 |
+
total_volume = 0
|
| 562 |
+
|
| 563 |
+
for p in prices:
|
| 564 |
+
# Try different field names for market cap
|
| 565 |
+
market_cap = (
|
| 566 |
+
p.get("market_cap") or
|
| 567 |
+
p.get("market_cap_usd") or
|
| 568 |
+
p.get("market_cap_rank") or # Sometimes this is the value
|
| 569 |
+
None
|
| 570 |
+
)
|
| 571 |
+
# If market_cap is not found, try calculating from price and supply
|
| 572 |
+
if not market_cap:
|
| 573 |
+
price = p.get("price") or p.get("current_price") or 0
|
| 574 |
+
supply = p.get("circulating_supply") or p.get("total_supply") or 0
|
| 575 |
+
if price and supply:
|
| 576 |
+
market_cap = float(price) * float(supply)
|
| 577 |
+
|
| 578 |
+
if market_cap:
|
| 579 |
+
try:
|
| 580 |
+
total_market_cap += float(market_cap)
|
| 581 |
+
except (TypeError, ValueError):
|
| 582 |
+
pass
|
| 583 |
+
|
| 584 |
+
# Try different field names for volume
|
| 585 |
+
volume = (
|
| 586 |
+
p.get("total_volume") or
|
| 587 |
+
p.get("volume_24h") or
|
| 588 |
+
p.get("volume_24h_usd") or
|
| 589 |
+
None
|
| 590 |
+
)
|
| 591 |
+
if volume:
|
| 592 |
+
try:
|
| 593 |
+
total_volume += float(volume)
|
| 594 |
+
except (TypeError, ValueError):
|
| 595 |
+
pass
|
| 596 |
+
|
| 597 |
+
logger.info(f"Market overview: {len(prices)} coins, total_market_cap={total_market_cap:,.0f}, total_volume={total_volume:,.0f}")
|
| 598 |
|
| 599 |
# Sort by 24h change
|
| 600 |
gainers = sorted(
|
|
|
|
| 615 |
"top_gainers": gainers,
|
| 616 |
"top_losers": losers,
|
| 617 |
"top_by_volume": sorted(prices, key=lambda x: x.get("total_volume", 0) or 0, reverse=True)[:5],
|
| 618 |
+
"timestamp": datetime.now().isoformat(),
|
| 619 |
+
"source": source
|
| 620 |
}
|
| 621 |
|
| 622 |
except HTTPException:
|
|
|
|
| 626 |
raise HTTPException(status_code=500, detail=str(e))
|
| 627 |
|
| 628 |
|
| 629 |
+
@app.get("/api/market")
|
| 630 |
+
async def get_market():
|
| 631 |
+
"""Get market data in format expected by frontend dashboard"""
|
| 632 |
+
try:
|
| 633 |
+
overview = await get_market_overview()
|
| 634 |
+
prices, source = await fetch_coingecko_prices(limit=50)
|
| 635 |
+
|
| 636 |
+
if not prices:
|
| 637 |
+
raise HTTPException(status_code=503, detail="Unable to fetch market data")
|
| 638 |
+
|
| 639 |
+
return {
|
| 640 |
+
"total_market_cap": overview.get("total_market_cap", 0),
|
| 641 |
+
"btc_dominance": overview.get("btc_dominance", 0),
|
| 642 |
+
"total_volume_24h": overview.get("total_volume_24h", 0),
|
| 643 |
+
"cryptocurrencies": prices,
|
| 644 |
+
"timestamp": datetime.now().isoformat(),
|
| 645 |
+
"source": source
|
| 646 |
+
}
|
| 647 |
+
except HTTPException:
|
| 648 |
+
raise
|
| 649 |
+
except Exception as e:
|
| 650 |
+
logger.error(f"Error in get_market: {e}")
|
| 651 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 652 |
+
|
| 653 |
+
|
| 654 |
+
@app.get("/api/trending")
|
| 655 |
+
async def get_trending():
|
| 656 |
+
"""Get trending cryptocurrencies (top gainers by 24h change)"""
|
| 657 |
+
try:
|
| 658 |
+
prices, source = await fetch_coingecko_prices(limit=100)
|
| 659 |
+
|
| 660 |
+
if not prices:
|
| 661 |
+
raise HTTPException(status_code=503, detail="Unable to fetch trending data")
|
| 662 |
+
|
| 663 |
+
trending = sorted(
|
| 664 |
+
[p for p in prices if p.get("price_change_percentage_24h") is not None],
|
| 665 |
+
key=lambda x: x.get("price_change_percentage_24h", 0),
|
| 666 |
+
reverse=True
|
| 667 |
+
)[:10]
|
| 668 |
+
|
| 669 |
+
return {
|
| 670 |
+
"trending": trending,
|
| 671 |
+
"count": len(trending),
|
| 672 |
+
"timestamp": datetime.now().isoformat(),
|
| 673 |
+
"source": source
|
| 674 |
+
}
|
| 675 |
+
except HTTPException:
|
| 676 |
+
raise
|
| 677 |
+
except Exception as e:
|
| 678 |
+
logger.error(f"Error in get_trending: {e}")
|
| 679 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 680 |
+
|
| 681 |
+
|
| 682 |
@app.get("/api/market/prices")
|
| 683 |
async def get_multiple_prices(symbols: str = Query("BTC,ETH,SOL", description="Comma-separated symbols")):
|
| 684 |
"""Get prices for multiple cryptocurrencies"""
|
|
|
|
| 687 |
|
| 688 |
# Fetch prices
|
| 689 |
prices_data = []
|
| 690 |
+
source = "binance"
|
| 691 |
for symbol in symbol_list:
|
| 692 |
try:
|
| 693 |
+
ticker, ticker_source = await fetch_binance_ticker(f"{symbol}USDT")
|
| 694 |
if ticker:
|
| 695 |
prices_data.append(ticker)
|
| 696 |
+
if ticker_source != "binance":
|
| 697 |
+
source = ticker_source
|
| 698 |
except:
|
| 699 |
continue
|
| 700 |
+
if not prices_data:
|
| 701 |
+
# Fallback to CoinGecko only – no local/mock data
|
| 702 |
+
prices_data, source = await fetch_coingecko_prices(symbol_list)
|
| 703 |
|
| 704 |
+
# If still empty, return an empty list with explicit source
|
| 705 |
if not prices_data:
|
| 706 |
+
source = "none"
|
|
|
|
| 707 |
|
| 708 |
return {
|
| 709 |
"symbols": symbol_list,
|
| 710 |
"count": len(prices_data),
|
| 711 |
"data": prices_data,
|
| 712 |
+
"source": source,
|
| 713 |
"timestamp": datetime.now().isoformat()
|
| 714 |
}
|
| 715 |
|
|
|
|
| 836 |
"""Get comprehensive scoring snapshot"""
|
| 837 |
try:
|
| 838 |
# Fetch data
|
| 839 |
+
ticker, _ = await fetch_binance_ticker(symbol)
|
| 840 |
ohlcv = await fetch_binance_ohlcv(symbol, "1h", 100)
|
| 841 |
|
| 842 |
if not ticker or not ohlcv:
|
|
|
|
| 896 |
|
| 897 |
@app.get("/api/sentiment")
|
| 898 |
async def get_sentiment():
|
| 899 |
+
"""Get market sentiment data.
|
| 900 |
+
|
| 901 |
+
This endpoint only returns a value when real news data is available.
|
| 902 |
+
If providers fail, we return an explicit HTTP 503 instead of synthesizing
|
| 903 |
+
sentiment from placeholder text.
|
| 904 |
+
"""
|
| 905 |
try:
|
| 906 |
news = await news_collector.get_latest_news(limit=5)
|
| 907 |
except CollectorError as exc:
|
| 908 |
+
logger.warning("Sentiment error due to news provider failure: %s", exc)
|
| 909 |
+
raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
|
| 910 |
+
|
| 911 |
+
if not news:
|
| 912 |
+
raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
|
| 913 |
+
|
| 914 |
+
text = " ".join(item.get("title", "") for item in news).strip()
|
| 915 |
+
if not text:
|
| 916 |
+
raise HTTPException(status_code=503, detail="Sentiment data is currently unavailable")
|
| 917 |
|
|
|
|
| 918 |
analysis = analyze_market_text(text)
|
| 919 |
score = analysis.get("signals", {}).get("crypto", {}).get("score", 0.0)
|
| 920 |
normalized_value = int((score + 1) * 50)
|
|
|
|
| 1041 |
@app.get("/api/hf/health")
|
| 1042 |
async def hf_health():
|
| 1043 |
"""HuggingFace integration health"""
|
| 1044 |
+
from ai_models import AI_MODELS_SUMMARY
|
| 1045 |
status = registry_status()
|
| 1046 |
+
status["models"] = AI_MODELS_SUMMARY
|
| 1047 |
status["timestamp"] = datetime.utcnow().isoformat()
|
| 1048 |
return status
|
| 1049 |
|
|
|
|
| 1051 |
@app.post("/api/hf/refresh")
|
| 1052 |
async def hf_refresh():
|
| 1053 |
"""Refresh HuggingFace data"""
|
| 1054 |
+
from ai_models import initialize_models
|
| 1055 |
result = initialize_models()
|
| 1056 |
+
return {"status": "ok" if result.get("models_loaded", 0) > 0 else "degraded", **result, "timestamp": datetime.utcnow().isoformat()}
|
| 1057 |
|
| 1058 |
|
| 1059 |
@app.get("/api/hf/registry")
|
|
|
|
| 1063 |
return {"kind": kind, "items": info.get("model_names", info)}
|
| 1064 |
|
| 1065 |
|
| 1066 |
+
@app.get("/api/resources/unified")
|
| 1067 |
+
async def get_unified_resources():
|
| 1068 |
+
"""Get unified API resources from crypto_resources_unified_2025-11-11.json"""
|
| 1069 |
+
try:
|
| 1070 |
+
data = local_resource_service.get_registry()
|
| 1071 |
+
if data:
|
| 1072 |
+
metadata = data.get("registry", {}).get("metadata", {})
|
| 1073 |
+
return {
|
| 1074 |
+
"success": True,
|
| 1075 |
+
"data": data,
|
| 1076 |
+
"metadata": metadata,
|
| 1077 |
+
"count": metadata.get("total_entries", 0),
|
| 1078 |
+
"fallback_assets": len(local_resource_service.get_supported_symbols())
|
| 1079 |
+
}
|
| 1080 |
+
return {"success": False, "error": "Resources file not found"}
|
| 1081 |
+
except Exception as e:
|
| 1082 |
+
logger.error(f"Error loading unified resources: {e}")
|
| 1083 |
+
return {"success": False, "error": str(e)}
|
| 1084 |
+
|
| 1085 |
+
|
| 1086 |
+
@app.get("/api/resources/ultimate")
|
| 1087 |
+
async def get_ultimate_resources():
|
| 1088 |
+
"""Get ultimate API resources from ultimate_crypto_pipeline_2025_NZasinich.json"""
|
| 1089 |
+
try:
|
| 1090 |
+
resources_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
|
| 1091 |
+
if resources_path.exists():
|
| 1092 |
+
with open(resources_path, 'r', encoding='utf-8') as f:
|
| 1093 |
+
data = json.load(f)
|
| 1094 |
+
return {
|
| 1095 |
+
"success": True,
|
| 1096 |
+
"data": data,
|
| 1097 |
+
"total_sources": data.get("total_sources", 0),
|
| 1098 |
+
"files": len(data.get("files", []))
|
| 1099 |
+
}
|
| 1100 |
+
return {"success": False, "error": "Resources file not found"}
|
| 1101 |
+
except Exception as e:
|
| 1102 |
+
logger.error(f"Error loading ultimate resources: {e}")
|
| 1103 |
+
return {"success": False, "error": str(e)}
|
| 1104 |
+
|
| 1105 |
+
|
| 1106 |
+
@app.get("/api/resources/stats")
|
| 1107 |
+
async def get_resources_stats():
|
| 1108 |
+
"""Get statistics about available API resources"""
|
| 1109 |
+
try:
|
| 1110 |
+
stats = {
|
| 1111 |
+
"unified": {"available": False, "count": 0},
|
| 1112 |
+
"ultimate": {"available": False, "count": 0},
|
| 1113 |
+
"total_apis": 0
|
| 1114 |
+
}
|
| 1115 |
+
|
| 1116 |
+
# Check unified resources via the centralized loader
|
| 1117 |
+
registry = local_resource_service.get_registry()
|
| 1118 |
+
if registry:
|
| 1119 |
+
stats["unified"] = {
|
| 1120 |
+
"available": True,
|
| 1121 |
+
"count": registry.get("registry", {}).get("metadata", {}).get("total_entries", 0),
|
| 1122 |
+
"fallback_assets": len(local_resource_service.get_supported_symbols())
|
| 1123 |
+
}
|
| 1124 |
+
|
| 1125 |
+
# Check ultimate resources
|
| 1126 |
+
ultimate_path = WORKSPACE_ROOT / "api-resources" / "ultimate_crypto_pipeline_2025_NZasinich.json"
|
| 1127 |
+
if ultimate_path.exists():
|
| 1128 |
+
with open(ultimate_path, 'r', encoding='utf-8') as f:
|
| 1129 |
+
ultimate_data = json.load(f)
|
| 1130 |
+
stats["ultimate"] = {
|
| 1131 |
+
"available": True,
|
| 1132 |
+
"count": ultimate_data.get("total_sources", 0)
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
stats["total_apis"] = stats["unified"].get("count", 0) + stats["ultimate"].get("count", 0)
|
| 1136 |
+
|
| 1137 |
+
return {"success": True, "stats": stats}
|
| 1138 |
+
except Exception as e:
|
| 1139 |
+
logger.error(f"Error getting resources stats: {e}")
|
| 1140 |
+
return {"success": False, "error": str(e)}
|
| 1141 |
+
|
| 1142 |
+
|
| 1143 |
def _resolve_sentiment_payload(payload: Union[List[str], Dict[str, Any]]) -> Dict[str, Any]:
|
| 1144 |
if isinstance(payload, list):
|
| 1145 |
return {"texts": payload, "mode": "auto"}
|
|
|
|
| 1158 |
@app.post("/api/hf/sentiment")
|
| 1159 |
async def hf_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
|
| 1160 |
"""Run sentiment analysis using shared AI helpers."""
|
| 1161 |
+
from ai_models import AI_MODELS_SUMMARY
|
| 1162 |
+
|
| 1163 |
+
if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
|
| 1164 |
+
return {
|
| 1165 |
+
"ok": False,
|
| 1166 |
+
"error": "No HF models are currently loaded.",
|
| 1167 |
+
"mode": AI_MODELS_SUMMARY.get("mode", "off"),
|
| 1168 |
+
"models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
|
| 1169 |
+
}
|
| 1170 |
|
| 1171 |
try:
|
| 1172 |
resolved = _resolve_sentiment_payload(payload)
|
|
|
|
| 1190 |
return {"mode": mode, "results": results, "timestamp": datetime.utcnow().isoformat()}
|
| 1191 |
|
| 1192 |
|
| 1193 |
+
@app.post("/api/hf/models/sentiment")
|
| 1194 |
+
async def hf_models_sentiment(payload: Union[List[str], Dict[str, Any]] = Body(...)):
|
| 1195 |
+
"""Compatibility endpoint for HF console sentiment panel."""
|
| 1196 |
+
from ai_models import AI_MODELS_SUMMARY
|
| 1197 |
+
|
| 1198 |
+
if AI_MODELS_SUMMARY.get("models_loaded", 0) == 0 or AI_MODELS_SUMMARY.get("mode") == "off":
|
| 1199 |
+
return {
|
| 1200 |
+
"ok": False,
|
| 1201 |
+
"error": "No HF models are currently loaded.",
|
| 1202 |
+
"mode": AI_MODELS_SUMMARY.get("mode", "off"),
|
| 1203 |
+
"models_loaded": AI_MODELS_SUMMARY.get("models_loaded", 0)
|
| 1204 |
+
}
|
| 1205 |
+
|
| 1206 |
+
return await hf_sentiment(payload)
|
| 1207 |
+
|
| 1208 |
+
|
| 1209 |
+
@app.post("/api/hf/models/forecast")
|
| 1210 |
+
async def hf_models_forecast(payload: Dict[str, Any] = Body(...)):
|
| 1211 |
+
"""Generate quick technical forecasts from provided closing prices."""
|
| 1212 |
+
series = payload.get("series") or payload.get("values") or payload.get("close")
|
| 1213 |
+
if not isinstance(series, list) or len(series) < 3:
|
| 1214 |
+
raise HTTPException(status_code=400, detail="Provide at least 3 closing prices in 'series'.")
|
| 1215 |
+
|
| 1216 |
+
try:
|
| 1217 |
+
floats = [float(x) for x in series]
|
| 1218 |
+
except (TypeError, ValueError) as exc:
|
| 1219 |
+
raise HTTPException(status_code=400, detail="Series must contain numeric values") from exc
|
| 1220 |
+
|
| 1221 |
+
model_name = (payload.get("model") or payload.get("model_name") or "btc_lstm").lower()
|
| 1222 |
+
steps = int(payload.get("steps") or 3)
|
| 1223 |
+
|
| 1224 |
+
deltas = [floats[i] - floats[i - 1] for i in range(1, len(floats))]
|
| 1225 |
+
avg_delta = mean(deltas)
|
| 1226 |
+
volatility = mean(abs(delta - avg_delta) for delta in deltas) if deltas else 0
|
| 1227 |
+
|
| 1228 |
+
predictions = []
|
| 1229 |
+
last = floats[-1]
|
| 1230 |
+
decay = 0.95 if model_name == "btc_arima" else 1.02
|
| 1231 |
+
for _ in range(steps):
|
| 1232 |
+
last = last + (avg_delta * decay)
|
| 1233 |
+
predictions.append(round(last, 4))
|
| 1234 |
+
|
| 1235 |
+
return {
|
| 1236 |
+
"model": model_name,
|
| 1237 |
+
"steps": steps,
|
| 1238 |
+
"input_count": len(floats),
|
| 1239 |
+
"volatility": round(volatility, 5),
|
| 1240 |
+
"predictions": predictions,
|
| 1241 |
+
"source": "local-fallback" if model_name == "btc_arima" else "hybrid",
|
| 1242 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
|
| 1246 |
+
@app.get("/api/hf/datasets/market/ohlcv")
|
| 1247 |
+
async def hf_dataset_market_ohlcv(symbol: str = Query("BTC"), interval: str = Query("1h"), limit: int = Query(120, ge=10, le=500)):
|
| 1248 |
+
"""Expose fallback OHLCV snapshots as a pseudo HF dataset slice."""
|
| 1249 |
+
data = local_resource_service.get_ohlcv(symbol.upper(), interval, limit)
|
| 1250 |
+
source = "local-fallback"
|
| 1251 |
+
|
| 1252 |
+
if not data:
|
| 1253 |
+
return {
|
| 1254 |
+
"symbol": symbol.upper(),
|
| 1255 |
+
"interval": interval,
|
| 1256 |
+
"count": 0,
|
| 1257 |
+
"data": [],
|
| 1258 |
+
"source": source,
|
| 1259 |
+
"message": "No cached OHLCV available yet"
|
| 1260 |
+
}
|
| 1261 |
+
|
| 1262 |
+
return {
|
| 1263 |
+
"symbol": symbol.upper(),
|
| 1264 |
+
"interval": interval,
|
| 1265 |
+
"count": len(data),
|
| 1266 |
+
"data": data,
|
| 1267 |
+
"source": source,
|
| 1268 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1269 |
+
}
|
| 1270 |
+
|
| 1271 |
+
|
| 1272 |
+
@app.get("/api/hf/datasets/market/btc_technical")
|
| 1273 |
+
async def hf_dataset_market_btc(limit: int = Query(50, ge=10, le=200)):
|
| 1274 |
+
"""Simplified technical metrics derived from fallback OHLCV data."""
|
| 1275 |
+
candles = local_resource_service.get_ohlcv("BTC", "1h", limit + 20)
|
| 1276 |
+
|
| 1277 |
+
if not candles:
|
| 1278 |
+
raise HTTPException(status_code=503, detail="Fallback OHLCV unavailable")
|
| 1279 |
+
|
| 1280 |
+
rows = []
|
| 1281 |
+
closes = [c["close"] for c in candles]
|
| 1282 |
+
for idx, candle in enumerate(candles[-limit:]):
|
| 1283 |
+
window = closes[max(0, idx): idx + 20]
|
| 1284 |
+
sma = sum(window) / len(window) if window else candle["close"]
|
| 1285 |
+
momentum = candle["close"] - candle["open"]
|
| 1286 |
+
rows.append({
|
| 1287 |
+
"timestamp": candle["timestamp"],
|
| 1288 |
+
"datetime": candle["datetime"],
|
| 1289 |
+
"close": candle["close"],
|
| 1290 |
+
"sma_20": round(sma, 4),
|
| 1291 |
+
"momentum": round(momentum, 4),
|
| 1292 |
+
"volatility": round((candle["high"] - candle["low"]) / candle["low"], 4)
|
| 1293 |
+
})
|
| 1294 |
+
|
| 1295 |
+
return {
|
| 1296 |
+
"symbol": "BTC",
|
| 1297 |
+
"interval": "1h",
|
| 1298 |
+
"count": len(rows),
|
| 1299 |
+
"items": rows,
|
| 1300 |
+
"source": "local-fallback"
|
| 1301 |
+
}
|
| 1302 |
+
|
| 1303 |
+
|
| 1304 |
+
@app.get("/api/hf/datasets/news/semantic")
|
| 1305 |
+
async def hf_dataset_news(limit: int = Query(10, ge=3, le=25)):
|
| 1306 |
+
"""News slice augmented with sentiment tags for HF demos."""
|
| 1307 |
+
try:
|
| 1308 |
+
news = await news_collector.get_latest_news(limit=limit)
|
| 1309 |
+
source = "providers"
|
| 1310 |
+
except CollectorError:
|
| 1311 |
+
news = []
|
| 1312 |
+
source = "local-fallback"
|
| 1313 |
+
|
| 1314 |
+
if not news:
|
| 1315 |
+
# No mock dataset here – return an empty list when providers have no data
|
| 1316 |
+
items = []
|
| 1317 |
+
source = "none"
|
| 1318 |
+
else:
|
| 1319 |
+
items = []
|
| 1320 |
+
for item in news:
|
| 1321 |
+
items.append({
|
| 1322 |
+
"title": item.get("title"),
|
| 1323 |
+
"source": item.get("source") or item.get("provider"),
|
| 1324 |
+
"sentiment": item.get("sentiment") or "neutral",
|
| 1325 |
+
"sentiment_score": item.get("sentiment_confidence", 0.5),
|
| 1326 |
+
"entities": item.get("symbols") or [],
|
| 1327 |
+
"summary": item.get("summary") or item.get("description"),
|
| 1328 |
+
"published_at": item.get("date") or item.get("published_at")
|
| 1329 |
+
})
|
| 1330 |
+
return {
|
| 1331 |
+
"count": len(items),
|
| 1332 |
+
"items": items,
|
| 1333 |
+
"source": source,
|
| 1334 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 1335 |
+
}
|
| 1336 |
+
|
| 1337 |
+
|
| 1338 |
# ============================================================================
|
| 1339 |
# HTML Routes - Serve UI files
|
| 1340 |
# ============================================================================
|
| 1341 |
|
| 1342 |
+
@app.get("/favicon.ico")
|
| 1343 |
+
async def favicon():
|
| 1344 |
+
"""Serve favicon"""
|
| 1345 |
+
favicon_path = WORKSPACE_ROOT / "static" / "favicon.ico"
|
| 1346 |
+
if favicon_path.exists():
|
| 1347 |
+
return FileResponse(favicon_path)
|
| 1348 |
+
return JSONResponse({"status": "no favicon"}, status_code=404)
|
| 1349 |
+
|
| 1350 |
@app.get("/", response_class=HTMLResponse)
|
| 1351 |
async def root():
|
| 1352 |
+
"""Serve main HTML UI page (index.html)"""
|
| 1353 |
+
index_path = WORKSPACE_ROOT / "index.html"
|
| 1354 |
+
if index_path.exists():
|
| 1355 |
+
return FileResponse(
|
| 1356 |
+
path=str(index_path),
|
| 1357 |
+
media_type="text/html",
|
| 1358 |
+
filename="index.html"
|
| 1359 |
+
)
|
| 1360 |
return HTMLResponse("<h1>Cryptocurrency Data & Analysis API</h1><p>See <a href='/docs'>/docs</a> for API documentation</p>")
|
| 1361 |
|
| 1362 |
@app.get("/index.html", response_class=HTMLResponse)
|
|
|
|
| 1377 |
@app.get("/admin.html", response_class=HTMLResponse)
|
| 1378 |
async def admin():
|
| 1379 |
"""Serve admin panel"""
|
| 1380 |
+
admin_path = WORKSPACE_ROOT / "admin.html"
|
| 1381 |
+
if admin_path.exists():
|
| 1382 |
+
return FileResponse(
|
| 1383 |
+
path=str(admin_path),
|
| 1384 |
+
media_type="text/html",
|
| 1385 |
+
filename="admin.html"
|
| 1386 |
+
)
|
| 1387 |
+
return HTMLResponse("<h1>Admin panel not found</h1>")
|
| 1388 |
|
| 1389 |
@app.get("/admin", response_class=HTMLResponse)
|
| 1390 |
async def admin_alt():
|
| 1391 |
"""Alternative route for admin"""
|
| 1392 |
+
admin_path = WORKSPACE_ROOT / "admin.html"
|
| 1393 |
+
if admin_path.exists():
|
| 1394 |
+
return FileResponse(
|
| 1395 |
+
path=str(admin_path),
|
| 1396 |
+
media_type="text/html",
|
| 1397 |
+
filename="admin.html"
|
| 1398 |
+
)
|
| 1399 |
+
return HTMLResponse("<h1>Admin panel not found</h1>")
|
| 1400 |
|
| 1401 |
@app.get("/hf_console.html", response_class=HTMLResponse)
|
| 1402 |
async def hf_console():
|
|
|
|
| 1437 |
# Startup Event
|
| 1438 |
# ============================================================================
|
| 1439 |
|
| 1440 |
+
|
| 1441 |
+
# ============================================================================
|
| 1442 |
+
# ADMIN DASHBOARD ENDPOINTS
|
| 1443 |
+
# ============================================================================
|
| 1444 |
+
|
| 1445 |
+
from fastapi import WebSocket, WebSocketDisconnect
|
| 1446 |
+
import asyncio
|
| 1447 |
+
|
| 1448 |
+
class ConnectionManager:
|
| 1449 |
+
def __init__(self):
|
| 1450 |
+
self.active_connections = []
|
| 1451 |
+
async def connect(self, websocket: WebSocket):
|
| 1452 |
+
await websocket.accept()
|
| 1453 |
+
self.active_connections.append(websocket)
|
| 1454 |
+
def disconnect(self, websocket: WebSocket):
|
| 1455 |
+
if websocket in self.active_connections:
|
| 1456 |
+
self.active_connections.remove(websocket)
|
| 1457 |
+
async def broadcast(self, message: dict):
|
| 1458 |
+
disconnected = []
|
| 1459 |
+
for conn in list(self.active_connections):
|
| 1460 |
+
try:
|
| 1461 |
+
# Check connection state before sending
|
| 1462 |
+
if conn.client_state == WebSocketState.CONNECTED:
|
| 1463 |
+
await conn.send_json(message)
|
| 1464 |
+
else:
|
| 1465 |
+
disconnected.append(conn)
|
| 1466 |
+
except Exception as e:
|
| 1467 |
+
logger.debug(f"Error broadcasting to client: {e}")
|
| 1468 |
+
disconnected.append(conn)
|
| 1469 |
+
|
| 1470 |
+
# Clean up disconnected clients
|
| 1471 |
+
for conn in disconnected:
|
| 1472 |
+
self.disconnect(conn)
|
| 1473 |
+
|
| 1474 |
+
ws_manager = ConnectionManager()
|
| 1475 |
+
|
| 1476 |
+
@app.get("/api/health")
|
| 1477 |
+
async def api_health():
|
| 1478 |
+
h = await health()
|
| 1479 |
+
return {"status": "healthy" if h.get("status") == "ok" else "degraded", **h}
|
| 1480 |
+
|
| 1481 |
+
# Removed duplicate - using improved version below
|
| 1482 |
+
|
| 1483 |
+
@app.get("/api/coins/{symbol}")
|
| 1484 |
+
async def get_coin_detail(symbol: str):
|
| 1485 |
+
coins = await market_collector.get_top_coins(limit=250)
|
| 1486 |
+
coin = next((c for c in coins if c.get("symbol", "").upper() == symbol.upper()), None)
|
| 1487 |
+
if not coin:
|
| 1488 |
+
raise HTTPException(404, f"Coin {symbol} not found")
|
| 1489 |
+
return {"success": True, "symbol": symbol.upper(), "name": coin.get("name", ""),
|
| 1490 |
+
"price": coin.get("price") or coin.get("current_price", 0),
|
| 1491 |
+
"change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
|
| 1492 |
+
"market_cap": coin.get("market_cap", 0)}
|
| 1493 |
+
|
| 1494 |
+
@app.get("/api/market/stats")
|
| 1495 |
+
async def get_market_stats():
|
| 1496 |
+
"""Get global market statistics (duplicate endpoint - keeping for compatibility)"""
|
| 1497 |
+
try:
|
| 1498 |
+
overview = await get_market_overview()
|
| 1499 |
+
|
| 1500 |
+
# Calculate ETH dominance from prices if available
|
| 1501 |
+
eth_dominance = 0
|
| 1502 |
+
if overview.get("total_market_cap", 0) > 0:
|
| 1503 |
+
try:
|
| 1504 |
+
eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
|
| 1505 |
+
if eth_prices and len(eth_prices) > 0:
|
| 1506 |
+
eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
|
| 1507 |
+
eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
|
| 1508 |
+
except:
|
| 1509 |
+
pass
|
| 1510 |
+
|
| 1511 |
+
return {
|
| 1512 |
+
"success": True,
|
| 1513 |
+
"stats": {
|
| 1514 |
+
"total_market_cap": overview.get("total_market_cap", 0) or 0,
|
| 1515 |
+
"total_volume_24h": overview.get("total_volume_24h", 0) or 0,
|
| 1516 |
+
"btc_dominance": overview.get("btc_dominance", 0) or 0,
|
| 1517 |
+
"eth_dominance": eth_dominance,
|
| 1518 |
+
"active_cryptocurrencies": 10000,
|
| 1519 |
+
"markets": 500,
|
| 1520 |
+
"market_cap_change_24h": 0.0,
|
| 1521 |
+
"timestamp": datetime.now().isoformat()
|
| 1522 |
+
}
|
| 1523 |
+
}
|
| 1524 |
+
except Exception as e:
|
| 1525 |
+
logger.error(f"Error in /api/market/stats (duplicate): {e}")
|
| 1526 |
+
# No fake zeroed stats – surface a clear error instead
|
| 1527 |
+
raise HTTPException(status_code=503, detail="Market statistics are currently unavailable")
|
| 1528 |
+
|
| 1529 |
+
|
| 1530 |
+
@app.get("/api/stats")
|
| 1531 |
+
async def get_stats_alias():
|
| 1532 |
+
"""Alias endpoint for /api/market/stats - backward compatibility"""
|
| 1533 |
+
return await get_market_stats()
|
| 1534 |
+
|
| 1535 |
+
|
| 1536 |
+
@app.get("/api/news/latest")
|
| 1537 |
+
async def get_latest_news(limit: int = Query(default=40, ge=1, le=100)):
|
| 1538 |
+
from ai_models import analyze_news_item
|
| 1539 |
+
news = await news_collector.get_latest_news(limit=limit)
|
| 1540 |
+
enriched = []
|
| 1541 |
+
for item in news[:limit]:
|
| 1542 |
+
try:
|
| 1543 |
+
e = analyze_news_item(item)
|
| 1544 |
+
enriched.append({"title": e.get("title", ""), "source": e.get("source", ""),
|
| 1545 |
+
"published_at": e.get("published_at") or e.get("date", ""),
|
| 1546 |
+
"symbols": e.get("symbols", []), "sentiment": e.get("sentiment", "neutral"),
|
| 1547 |
+
"sentiment_confidence": e.get("sentiment_confidence", 0.5)})
|
| 1548 |
+
except:
|
| 1549 |
+
enriched.append({"title": item.get("title", ""), "source": item.get("source", ""),
|
| 1550 |
+
"published_at": item.get("date", ""), "symbols": item.get("symbols", []),
|
| 1551 |
+
"sentiment": "neutral", "sentiment_confidence": 0.5})
|
| 1552 |
+
return {"success": True, "news": enriched, "count": len(enriched)}
|
| 1553 |
+
|
| 1554 |
+
@app.post("/api/news/summarize")
|
| 1555 |
+
async def summarize_news(item: Dict[str, Any] = Body(...)):
|
| 1556 |
+
from ai_models import analyze_news_item
|
| 1557 |
+
e = analyze_news_item(item)
|
| 1558 |
+
return {"success": True, "summary": e.get("title", ""), "sentiment": e.get("sentiment", "neutral")}
|
| 1559 |
+
|
| 1560 |
+
# Duplicate endpoints removed - using the improved versions below in CHARTS ENDPOINTS section
|
| 1561 |
+
|
| 1562 |
+
@app.post("/api/sentiment/analyze")
|
| 1563 |
+
async def analyze_sentiment(payload: Dict[str, Any] = Body(...)):
|
| 1564 |
+
from ai_models import ensemble_crypto_sentiment
|
| 1565 |
+
result = ensemble_crypto_sentiment(payload.get("text", ""))
|
| 1566 |
+
return {"success": True, "sentiment": result["label"], "confidence": result["confidence"], "details": result}
|
| 1567 |
+
|
| 1568 |
+
@app.post("/api/query")
|
| 1569 |
+
async def process_query(payload: Dict[str, Any] = Body(...)):
|
| 1570 |
+
query = payload.get("query", "").lower()
|
| 1571 |
+
if "price" in query or "btc" in query:
|
| 1572 |
+
coins = await market_collector.get_top_coins(limit=10)
|
| 1573 |
+
btc = next((c for c in coins if c.get("symbol", "").upper() == "BTC"), None)
|
| 1574 |
+
if btc:
|
| 1575 |
+
return {"success": True, "type": "price", "message": f"Bitcoin is ${btc.get('price', 0):,.2f}", "data": btc}
|
| 1576 |
+
return {"success": True, "type": "general", "message": "Query processed"}
|
| 1577 |
+
|
| 1578 |
+
@app.get("/api/datasets/list")
|
| 1579 |
+
async def list_datasets():
|
| 1580 |
+
from backend.services.hf_registry import REGISTRY
|
| 1581 |
+
datasets = REGISTRY.list(kind="datasets")
|
| 1582 |
+
formatted = [{"name": d.get("id"), "category": d.get("category", "other"), "tags": d.get("tags", [])} for d in datasets]
|
| 1583 |
+
return {"success": True, "datasets": formatted, "count": len(formatted)}
|
| 1584 |
+
|
| 1585 |
+
@app.get("/api/datasets/sample")
|
| 1586 |
+
async def get_dataset_sample(name: str = Query(...), limit: int = Query(default=20)):
|
| 1587 |
+
return {"success": False, "name": name, "sample": [], "message": "Auth required"}
|
| 1588 |
+
|
| 1589 |
+
@app.get("/api/models/list")
|
| 1590 |
+
async def list_models():
|
| 1591 |
+
from ai_models import get_model_info
|
| 1592 |
+
info = get_model_info()
|
| 1593 |
+
models = []
|
| 1594 |
+
for cat, mlist in info.get("model_catalog", {}).items():
|
| 1595 |
+
for mid in mlist:
|
| 1596 |
+
models.append({"name": mid, "task": "sentiment" if "sentiment" in cat else "analysis", "category": cat})
|
| 1597 |
+
return {"success": True, "models": models, "count": len(models)}
|
| 1598 |
+
|
| 1599 |
+
@app.post("/api/models/test")
|
| 1600 |
+
async def test_model(payload: Dict[str, Any] = Body(...)):
|
| 1601 |
+
from ai_models import ensemble_crypto_sentiment
|
| 1602 |
+
result = ensemble_crypto_sentiment(payload.get("text", ""))
|
| 1603 |
+
return {"success": True, "model": payload.get("model", ""), "result": result}
|
| 1604 |
+
|
| 1605 |
+
@app.websocket("/ws")
|
| 1606 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 1607 |
+
await ws_manager.connect(websocket)
|
| 1608 |
+
try:
|
| 1609 |
+
while True:
|
| 1610 |
+
# Check if connection is still open before sending
|
| 1611 |
+
if websocket.client_state != WebSocketState.CONNECTED:
|
| 1612 |
+
logger.info("WebSocket connection closed, breaking loop")
|
| 1613 |
+
break
|
| 1614 |
+
|
| 1615 |
+
try:
|
| 1616 |
+
top_coins = await market_collector.get_top_coins(limit=5)
|
| 1617 |
+
news = await news_collector.get_latest_news(limit=3)
|
| 1618 |
+
from ai_models import ensemble_crypto_sentiment
|
| 1619 |
+
sentiment = ensemble_crypto_sentiment(" ".join([n.get("title", "") for n in news])) if news else {"label": "neutral", "confidence": 0.5}
|
| 1620 |
+
|
| 1621 |
+
# Double-check connection state before sending
|
| 1622 |
+
if websocket.client_state == WebSocketState.CONNECTED:
|
| 1623 |
+
await websocket.send_json({
|
| 1624 |
+
"type": "update",
|
| 1625 |
+
"payload": {
|
| 1626 |
+
"market_data": top_coins,
|
| 1627 |
+
"news": news,
|
| 1628 |
+
"sentiment": sentiment,
|
| 1629 |
+
"timestamp": datetime.now().isoformat()
|
| 1630 |
+
}
|
| 1631 |
+
})
|
| 1632 |
+
else:
|
| 1633 |
+
logger.info("WebSocket disconnected, breaking loop")
|
| 1634 |
+
break
|
| 1635 |
+
|
| 1636 |
+
except CollectorError as e:
|
| 1637 |
+
# Provider errors are already logged by the collector, just continue
|
| 1638 |
+
logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
|
| 1639 |
+
# Use cached data if available, or empty data
|
| 1640 |
+
top_coins = []
|
| 1641 |
+
news = []
|
| 1642 |
+
sentiment = {"label": "neutral", "confidence": 0.5}
|
| 1643 |
+
except Exception as e:
|
| 1644 |
+
# Log other errors with full details
|
| 1645 |
+
error_msg = str(e) if str(e) else repr(e)
|
| 1646 |
+
logger.error(f"Error in WebSocket update loop: {type(e).__name__}: {error_msg}")
|
| 1647 |
+
# Don't break on data errors, just log and continue
|
| 1648 |
+
# Only break on connection errors
|
| 1649 |
+
if "send" in str(e).lower() or "close" in str(e).lower():
|
| 1650 |
+
break
|
| 1651 |
+
|
| 1652 |
+
await asyncio.sleep(10)
|
| 1653 |
+
except WebSocketDisconnect:
|
| 1654 |
+
logger.info("WebSocket disconnect exception caught")
|
| 1655 |
+
except Exception as e:
|
| 1656 |
+
logger.error(f"WebSocket endpoint error: {e}")
|
| 1657 |
+
finally:
|
| 1658 |
+
try:
|
| 1659 |
+
ws_manager.disconnect(websocket)
|
| 1660 |
+
except:
|
| 1661 |
+
pass
|
| 1662 |
+
|
| 1663 |
+
|
| 1664 |
@app.on_event("startup")
|
| 1665 |
async def startup_event():
|
| 1666 |
+
"""Initialize on startup - non-blocking"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1667 |
logger.info("=" * 70)
|
| 1668 |
+
logger.info("Starting Cryptocurrency Data & Analysis API")
|
| 1669 |
logger.info("=" * 70)
|
| 1670 |
+
logger.info("FastAPI initialized")
|
| 1671 |
+
logger.info("CORS configured")
|
| 1672 |
+
logger.info("Cache initialized")
|
| 1673 |
+
logger.info(f"Providers loaded: {len(PROVIDERS_CONFIG)}")
|
| 1674 |
+
|
| 1675 |
+
# Initialize AI models in background (non-blocking)
|
| 1676 |
+
async def init_models_background():
|
| 1677 |
+
try:
|
| 1678 |
+
from ai_models import initialize_models
|
| 1679 |
+
models_init = initialize_models()
|
| 1680 |
+
logger.info(f"AI Models initialized: {models_init}")
|
| 1681 |
+
except Exception as e:
|
| 1682 |
+
logger.warning(f"AI Models initialization failed: {e}")
|
| 1683 |
+
|
| 1684 |
+
# Initialize HF Registry in background (non-blocking)
|
| 1685 |
+
async def init_registry_background():
|
| 1686 |
+
try:
|
| 1687 |
+
from backend.services.hf_registry import REGISTRY
|
| 1688 |
+
registry_result = await REGISTRY.refresh()
|
| 1689 |
+
logger.info(f"HF Registry initialized: {registry_result}")
|
| 1690 |
+
except Exception as e:
|
| 1691 |
+
logger.warning(f"HF Registry initialization failed: {e}")
|
| 1692 |
+
|
| 1693 |
+
# Start background tasks
|
| 1694 |
+
asyncio.create_task(init_models_background())
|
| 1695 |
+
asyncio.create_task(init_registry_background())
|
| 1696 |
+
logger.info("Background initialization tasks started")
|
| 1697 |
|
| 1698 |
# Show loaded HuggingFace Space providers
|
| 1699 |
hf_providers = [p for p in PROVIDERS_CONFIG.keys() if 'huggingface_space' in p]
|
| 1700 |
if hf_providers:
|
| 1701 |
+
logger.info(f"HuggingFace Space providers: {', '.join(hf_providers)}")
|
| 1702 |
|
| 1703 |
+
logger.info("Data sources: Binance, CoinGecko, providers_config_extended.json")
|
| 1704 |
|
| 1705 |
# Check HTML files
|
| 1706 |
html_files = ["index.html", "dashboard.html", "admin.html", "hf_console.html"]
|
| 1707 |
available_html = [f for f in html_files if (WORKSPACE_ROOT / f).exists()]
|
| 1708 |
+
logger.info(f"UI files: {len(available_html)}/{len(html_files)} available")
|
| 1709 |
+
logger.info(f"HTML UI available at: http://0.0.0.0:7860/ (index.html)")
|
| 1710 |
|
| 1711 |
logger.info("=" * 70)
|
| 1712 |
+
logger.info("API ready at http://0.0.0.0:7860")
|
| 1713 |
+
logger.info("Docs at http://0.0.0.0:7860/docs")
|
| 1714 |
+
logger.info("UI at http://0.0.0.0:7860/ (index.html - default HTML page)")
|
| 1715 |
logger.info("=" * 70)
|
| 1716 |
|
| 1717 |
|
|
|
|
| 1721 |
|
| 1722 |
if __name__ == "__main__":
|
| 1723 |
import uvicorn
|
| 1724 |
+
import sys
|
| 1725 |
+
import io
|
| 1726 |
|
| 1727 |
+
# Fix encoding for Windows console
|
| 1728 |
+
if sys.platform == "win32":
|
| 1729 |
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
| 1730 |
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
| 1731 |
+
|
| 1732 |
+
try:
|
| 1733 |
+
print("=" * 70)
|
| 1734 |
+
print("Starting Cryptocurrency Data & Analysis API")
|
| 1735 |
+
print("=" * 70)
|
| 1736 |
+
print("Server: http://localhost:7860")
|
| 1737 |
+
print("API Docs: http://localhost:7860/docs")
|
| 1738 |
+
print("Health: http://localhost:7860/health")
|
| 1739 |
+
print("=" * 70)
|
| 1740 |
+
except UnicodeEncodeError:
|
| 1741 |
+
# Fallback if encoding still fails
|
| 1742 |
+
print("=" * 70)
|
| 1743 |
+
print("Starting Cryptocurrency Data & Analysis API")
|
| 1744 |
+
print("=" * 70)
|
| 1745 |
+
print("Server: http://localhost:7860")
|
| 1746 |
+
print("API Docs: http://localhost:7860/docs")
|
| 1747 |
+
print("Health: http://localhost:7860/health")
|
| 1748 |
+
print("=" * 70)
|
| 1749 |
|
| 1750 |
uvicorn.run(
|
| 1751 |
app,
|
|
|
|
| 1774 |
logger.info(f"WebSocket disconnected. Total: {len(self.active_connections)}")
|
| 1775 |
|
| 1776 |
async def broadcast(self, message: dict):
|
| 1777 |
+
disconnected = []
|
| 1778 |
for connection in list(self.active_connections):
|
| 1779 |
try:
|
| 1780 |
+
# Check connection state before sending
|
| 1781 |
+
if connection.client_state == WebSocketState.CONNECTED:
|
| 1782 |
+
await connection.send_json(message)
|
| 1783 |
+
else:
|
| 1784 |
+
disconnected.append(connection)
|
| 1785 |
+
except Exception as e:
|
| 1786 |
+
logger.debug(f"Error broadcasting to client: {e}")
|
| 1787 |
+
disconnected.append(connection)
|
| 1788 |
+
|
| 1789 |
+
# Clean up disconnected clients
|
| 1790 |
+
for connection in disconnected:
|
| 1791 |
+
self.disconnect(connection)
|
| 1792 |
|
| 1793 |
ws_manager = ConnectionManager()
|
| 1794 |
|
|
|
|
| 1814 |
result = []
|
| 1815 |
for coin in coins:
|
| 1816 |
result.append({
|
| 1817 |
+
"id": coin.get("id", coin.get("symbol", "").lower()),
|
| 1818 |
"rank": coin.get("rank", 0),
|
| 1819 |
"symbol": coin.get("symbol", "").upper(),
|
| 1820 |
"name": coin.get("name", ""),
|
| 1821 |
"price": coin.get("price") or coin.get("current_price", 0),
|
| 1822 |
+
"current_price": coin.get("price") or coin.get("current_price", 0),
|
| 1823 |
"price_change_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
|
| 1824 |
+
"price_change_percentage_24h": coin.get("change_24h") or coin.get("price_change_percentage_24h", 0),
|
| 1825 |
+
"price_change_percentage_7d_in_currency": coin.get("price_change_percentage_7d", 0),
|
| 1826 |
"volume_24h": coin.get("volume_24h") or coin.get("total_volume", 0),
|
| 1827 |
+
"total_volume": coin.get("volume_24h") or coin.get("total_volume", 0),
|
| 1828 |
"market_cap": coin.get("market_cap", 0),
|
| 1829 |
"image": coin.get("image", ""),
|
| 1830 |
+
"sparkline_in_7d": coin.get("sparkline_in_7d") or {"price": []},
|
| 1831 |
+
"sparkline_data": coin.get("sparkline_data") or [],
|
| 1832 |
"last_updated": coin.get("last_updated", datetime.now().isoformat())
|
| 1833 |
})
|
| 1834 |
|
|
|
|
| 1876 |
async def get_market_stats():
|
| 1877 |
"""Get global market statistics"""
|
| 1878 |
try:
|
| 1879 |
+
# Use existing endpoint - get_market_overview returns total_market_cap and total_volume_24h
|
| 1880 |
overview = await get_market_overview()
|
| 1881 |
|
| 1882 |
+
# Calculate ETH dominance from prices if available
|
| 1883 |
+
eth_dominance = 0
|
| 1884 |
+
if overview.get("total_market_cap", 0) > 0:
|
| 1885 |
+
# Try to get ETH market cap from top coins
|
| 1886 |
+
try:
|
| 1887 |
+
eth_prices, _ = await fetch_coingecko_prices(symbols=["ETH"], limit=1)
|
| 1888 |
+
if eth_prices and len(eth_prices) > 0:
|
| 1889 |
+
eth_market_cap = eth_prices[0].get("market_cap", 0) or 0
|
| 1890 |
+
eth_dominance = (eth_market_cap / overview.get("total_market_cap", 1)) * 100
|
| 1891 |
+
except:
|
| 1892 |
+
pass
|
| 1893 |
+
|
| 1894 |
stats = {
|
| 1895 |
+
"total_market_cap": overview.get("total_market_cap", 0) or 0,
|
| 1896 |
+
"total_volume_24h": overview.get("total_volume_24h", 0) or 0,
|
| 1897 |
+
"btc_dominance": overview.get("btc_dominance", 0) or 0,
|
| 1898 |
+
"eth_dominance": eth_dominance,
|
| 1899 |
"active_cryptocurrencies": 10000, # Approximate
|
| 1900 |
"markets": 500, # Approximate
|
| 1901 |
"market_cap_change_24h": 0.0,
|
|
|
|
| 1952 |
return {"success": True, "news": [], "count": 0, "timestamp": datetime.now().isoformat()}
|
| 1953 |
|
| 1954 |
|
| 1955 |
+
@app.get("/api/news")
|
| 1956 |
+
async def get_news(limit: int = Query(default=40, ge=1, le=100)):
|
| 1957 |
+
"""Alias for /api/news/latest for backward compatibility"""
|
| 1958 |
+
return await get_latest_news(limit=limit)
|
| 1959 |
+
|
| 1960 |
+
|
| 1961 |
@app.post("/api/news/summarize")
|
| 1962 |
async def summarize_news(item: Dict[str, Any] = Body(...)):
|
| 1963 |
"""Summarize a news article"""
|
|
|
|
| 1986 |
async def get_price_chart(symbol: str, timeframe: str = Query(default="7d")):
|
| 1987 |
"""Get price chart data"""
|
| 1988 |
try:
|
| 1989 |
+
# Clean and validate symbol
|
| 1990 |
+
symbol = symbol.strip().upper()
|
| 1991 |
+
if not symbol:
|
| 1992 |
+
return JSONResponse(
|
| 1993 |
+
status_code=400,
|
| 1994 |
+
content={
|
| 1995 |
+
"success": False,
|
| 1996 |
+
"symbol": "",
|
| 1997 |
+
"timeframe": timeframe,
|
| 1998 |
+
"data": [],
|
| 1999 |
+
"count": 0,
|
| 2000 |
+
"error": "Symbol cannot be empty"
|
| 2001 |
+
}
|
| 2002 |
+
)
|
| 2003 |
|
| 2004 |
+
logger.info(f"Fetching price history for {symbol} with timeframe {timeframe}")
|
| 2005 |
+
|
| 2006 |
+
# market_collector.get_price_history expects timeframe as string, not hours
|
| 2007 |
+
price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
|
| 2008 |
+
|
| 2009 |
+
if not price_history or len(price_history) == 0:
|
| 2010 |
+
logger.warning(f"No price history returned for {symbol}")
|
| 2011 |
+
return {
|
| 2012 |
+
"success": True,
|
| 2013 |
+
"symbol": symbol,
|
| 2014 |
+
"timeframe": timeframe,
|
| 2015 |
+
"data": [],
|
| 2016 |
+
"count": 0,
|
| 2017 |
+
"message": "No data available"
|
| 2018 |
+
}
|
| 2019 |
|
| 2020 |
chart_data = []
|
| 2021 |
for point in price_history:
|
| 2022 |
+
# Handle different timestamp formats
|
| 2023 |
+
timestamp = point.get("timestamp") or point.get("time") or point.get("date")
|
| 2024 |
+
price = point.get("price") or point.get("close") or point.get("value") or 0
|
| 2025 |
+
|
| 2026 |
+
# Convert timestamp to ISO format if needed
|
| 2027 |
+
if timestamp:
|
| 2028 |
+
try:
|
| 2029 |
+
# If it's already a string, use it
|
| 2030 |
+
if isinstance(timestamp, str):
|
| 2031 |
+
# Try to parse and format
|
| 2032 |
+
try:
|
| 2033 |
+
# Try ISO format first
|
| 2034 |
+
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
|
| 2035 |
+
timestamp = dt.isoformat()
|
| 2036 |
+
except:
|
| 2037 |
+
try:
|
| 2038 |
+
# Try other common formats
|
| 2039 |
+
from dateutil import parser
|
| 2040 |
+
dt = parser.parse(timestamp)
|
| 2041 |
+
timestamp = dt.isoformat()
|
| 2042 |
+
except:
|
| 2043 |
+
pass
|
| 2044 |
+
elif isinstance(timestamp, (int, float)):
|
| 2045 |
+
# Unix timestamp
|
| 2046 |
+
dt = datetime.fromtimestamp(timestamp)
|
| 2047 |
+
timestamp = dt.isoformat()
|
| 2048 |
+
except Exception as e:
|
| 2049 |
+
logger.warning(f"Error parsing timestamp {timestamp}: {e}")
|
| 2050 |
+
|
| 2051 |
chart_data.append({
|
| 2052 |
+
"timestamp": timestamp or "",
|
| 2053 |
+
"time": timestamp or "",
|
| 2054 |
+
"date": timestamp or "",
|
| 2055 |
+
"price": float(price) if price else 0,
|
| 2056 |
+
"close": float(price) if price else 0,
|
| 2057 |
+
"value": float(price) if price else 0
|
| 2058 |
})
|
| 2059 |
|
| 2060 |
+
logger.info(f"Returning {len(chart_data)} data points for {symbol}")
|
| 2061 |
+
|
| 2062 |
return {
|
| 2063 |
"success": True,
|
| 2064 |
+
"symbol": symbol,
|
| 2065 |
"timeframe": timeframe,
|
| 2066 |
"data": chart_data,
|
| 2067 |
"count": len(chart_data)
|
| 2068 |
}
|
| 2069 |
+
except CollectorError as e:
|
| 2070 |
+
logger.error(f"Collector error in /api/charts/price/{symbol}: {e}", exc_info=True)
|
| 2071 |
+
return JSONResponse(
|
| 2072 |
+
status_code=200,
|
| 2073 |
+
content={
|
| 2074 |
+
"success": False,
|
| 2075 |
+
"symbol": symbol.upper() if symbol else "",
|
| 2076 |
+
"timeframe": timeframe,
|
| 2077 |
+
"data": [],
|
| 2078 |
+
"count": 0,
|
| 2079 |
+
"error": str(e)
|
| 2080 |
+
}
|
| 2081 |
+
)
|
| 2082 |
except Exception as e:
|
| 2083 |
+
logger.error(f"Error in /api/charts/price/{symbol}: {e}", exc_info=True)
|
| 2084 |
+
return JSONResponse(
|
| 2085 |
+
status_code=200,
|
| 2086 |
+
content={
|
| 2087 |
+
"success": False,
|
| 2088 |
+
"symbol": symbol.upper() if symbol else "",
|
| 2089 |
+
"timeframe": timeframe,
|
| 2090 |
+
"data": [],
|
| 2091 |
+
"count": 0,
|
| 2092 |
+
"error": str(e)
|
| 2093 |
+
}
|
| 2094 |
+
)
|
| 2095 |
|
| 2096 |
|
| 2097 |
@app.post("/api/charts/analyze")
|
|
|
|
| 2102 |
timeframe = payload.get("timeframe", "7d")
|
| 2103 |
indicators = payload.get("indicators", [])
|
| 2104 |
|
| 2105 |
+
if not symbol:
|
| 2106 |
+
return JSONResponse(
|
| 2107 |
+
status_code=400,
|
| 2108 |
+
content={"success": False, "error": "Symbol is required"}
|
| 2109 |
+
)
|
| 2110 |
+
|
| 2111 |
+
symbol = symbol.strip().upper()
|
| 2112 |
+
logger.info(f"Analyzing chart for {symbol} with timeframe {timeframe}")
|
| 2113 |
+
|
| 2114 |
+
# Get price data - use timeframe string, not hours
|
| 2115 |
+
price_history = await market_collector.get_price_history(symbol, timeframe=timeframe)
|
| 2116 |
+
|
| 2117 |
+
if not price_history or len(price_history) == 0:
|
| 2118 |
+
return {
|
| 2119 |
+
"success": False,
|
| 2120 |
+
"symbol": symbol,
|
| 2121 |
+
"timeframe": timeframe,
|
| 2122 |
+
"error": "No price data available for analysis"
|
| 2123 |
+
}
|
| 2124 |
|
| 2125 |
# Analyze with AI
|
| 2126 |
from ai_models import analyze_chart_points
|
| 2127 |
+
try:
|
| 2128 |
+
analysis = analyze_chart_points(price_history, indicators)
|
| 2129 |
+
except Exception as ai_error:
|
| 2130 |
+
logger.error(f"AI analysis error: {ai_error}", exc_info=True)
|
| 2131 |
+
# Return a basic analysis if AI fails
|
| 2132 |
+
analysis = {
|
| 2133 |
+
"direction": "neutral",
|
| 2134 |
+
"summary": "Analysis unavailable",
|
| 2135 |
+
"signals": []
|
| 2136 |
+
}
|
| 2137 |
|
| 2138 |
return {
|
| 2139 |
"success": True,
|
|
|
|
| 2141 |
"timeframe": timeframe,
|
| 2142 |
"analysis": analysis
|
| 2143 |
}
|
| 2144 |
+
except CollectorError as e:
|
| 2145 |
+
logger.error(f"Collector error in /api/charts/analyze: {e}", exc_info=True)
|
| 2146 |
+
return JSONResponse(
|
| 2147 |
+
status_code=200,
|
| 2148 |
+
content={"success": False, "error": str(e)}
|
| 2149 |
+
)
|
| 2150 |
except Exception as e:
|
| 2151 |
+
logger.error(f"Error in /api/charts/analyze: {e}", exc_info=True)
|
| 2152 |
+
return JSONResponse(
|
| 2153 |
+
status_code=200,
|
| 2154 |
+
content={"success": False, "error": str(e)}
|
| 2155 |
+
)
|
| 2156 |
|
| 2157 |
|
| 2158 |
# ===== SENTIMENT ENDPOINTS =====
|
|
|
|
| 2244 |
# Attempt to load dataset
|
| 2245 |
try:
|
| 2246 |
from datasets import load_dataset
|
| 2247 |
+
from config import get_settings
|
| 2248 |
+
|
| 2249 |
+
# Get HF token for dataset loading
|
| 2250 |
+
settings = get_settings()
|
| 2251 |
+
hf_token = settings.hf_token or "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"
|
| 2252 |
+
|
| 2253 |
+
# Set token in environment for datasets library
|
| 2254 |
+
import os
|
| 2255 |
+
if hf_token and not os.environ.get("HF_TOKEN"):
|
| 2256 |
+
os.environ["HF_TOKEN"] = hf_token
|
| 2257 |
+
|
| 2258 |
+
dataset = load_dataset(name, split="train", streaming=True, token=hf_token)
|
| 2259 |
|
| 2260 |
sample = []
|
| 2261 |
for i, row in enumerate(dataset):
|
|
|
|
| 2340 |
|
| 2341 |
try:
|
| 2342 |
while True:
|
| 2343 |
+
# Check if connection is still open before sending
|
| 2344 |
+
if websocket.client_state != WebSocketState.CONNECTED:
|
| 2345 |
+
logger.info("WebSocket connection closed, breaking loop")
|
| 2346 |
+
break
|
| 2347 |
+
|
| 2348 |
# Send market updates every 10 seconds
|
| 2349 |
try:
|
| 2350 |
# Get latest data
|
|
|
|
| 2367 |
"timestamp": datetime.now().isoformat()
|
| 2368 |
}
|
| 2369 |
|
| 2370 |
+
# Double-check connection state before sending
|
| 2371 |
+
if websocket.client_state == WebSocketState.CONNECTED:
|
| 2372 |
+
await websocket.send_json({
|
| 2373 |
+
"type": "update",
|
| 2374 |
+
"payload": payload
|
| 2375 |
+
})
|
| 2376 |
+
else:
|
| 2377 |
+
logger.info("WebSocket disconnected, breaking loop")
|
| 2378 |
+
break
|
| 2379 |
+
except CollectorError as e:
|
| 2380 |
+
# Provider errors are already logged by the collector, just continue
|
| 2381 |
+
logger.debug(f"Provider error in WebSocket update (this is expected with fallbacks): {e}")
|
| 2382 |
+
# Use empty data on provider errors
|
| 2383 |
+
payload = {
|
| 2384 |
+
"market_data": [],
|
| 2385 |
+
"stats": {"total_market_cap": 0, "sentiment": {"label": "neutral", "confidence": 0.5}},
|
| 2386 |
+
"news": [],
|
| 2387 |
+
"sentiment": {"label": "neutral", "confidence": 0.5},
|
| 2388 |
+
"timestamp": datetime.now().isoformat()
|
| 2389 |
+
}
|
| 2390 |
except Exception as e:
|
| 2391 |
+
# Log other errors with full details
|
| 2392 |
+
error_msg = str(e) if str(e) else repr(e)
|
| 2393 |
+
logger.error(f"Error in WebSocket update: {type(e).__name__}: {error_msg}")
|
| 2394 |
+
# Don't break on data errors, just log and continue
|
| 2395 |
+
# Only break on connection errors
|
| 2396 |
+
if "send" in str(e).lower() or "close" in str(e).lower():
|
| 2397 |
+
break
|
| 2398 |
|
| 2399 |
await asyncio.sleep(10)
|
| 2400 |
except WebSocketDisconnect:
|
| 2401 |
+
logger.info("WebSocket disconnect exception caught")
|
| 2402 |
except Exception as e:
|
| 2403 |
logger.error(f"WebSocket error: {e}")
|
| 2404 |
+
finally:
|
| 2405 |
+
try:
|
| 2406 |
+
ws_manager.disconnect(websocket)
|
| 2407 |
+
except:
|
| 2408 |
+
pass
|
index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
main.py
CHANGED
|
@@ -1,31 +1,43 @@
|
|
| 1 |
"""
|
| 2 |
-
Main entry point for HuggingFace Space
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
from pathlib import Path
|
| 6 |
import sys
|
| 7 |
|
| 8 |
-
#
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
# Import the unified server app
|
| 13 |
try:
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
#
|
|
|
|
| 19 |
from fastapi import FastAPI
|
| 20 |
-
|
| 21 |
-
|
|
|
|
| 22 |
@app.get("/health")
|
| 23 |
def health():
|
| 24 |
-
return {
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
@app.get("/")
|
| 27 |
def root():
|
| 28 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
# Export app for uvicorn
|
| 31 |
__all__ = ["app"]
|
|
|
|
| 1 |
"""
|
| 2 |
+
Main entry point for HuggingFace Space / local Uvicorn.
|
| 3 |
+
This file exposes `app` so that:
|
| 4 |
+
* Hugging Face Spaces can auto-discover it (`main:app`)
|
| 5 |
+
* You can run locally via: uvicorn main:app --host 0.0.0.0 --port 7860
|
| 6 |
"""
|
| 7 |
from pathlib import Path
|
| 8 |
import sys
|
| 9 |
|
| 10 |
+
# Ensure project root is importable
|
| 11 |
+
CURRENT_DIR = Path(__file__).resolve().parent
|
| 12 |
+
if str(CURRENT_DIR) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(CURRENT_DIR))
|
| 14 |
+
|
| 15 |
+
_import_error = None
|
| 16 |
|
|
|
|
| 17 |
try:
|
| 18 |
+
# Canonical unified FastAPI backend with all endpoints used by index.html
|
| 19 |
+
from hf_unified_server import app # type: ignore
|
| 20 |
+
except Exception as e: # pragma: no cover - only used when something is really wrong
|
| 21 |
+
_import_error = e
|
| 22 |
+
# Minimal fallback app so deployment never hard-crashes,
|
| 23 |
+
# but clearly reports what went wrong.
|
| 24 |
from fastapi import FastAPI
|
| 25 |
+
|
| 26 |
+
app = FastAPI(title="Crypto Intelligence Hub - Fallback")
|
| 27 |
+
|
| 28 |
@app.get("/health")
|
| 29 |
def health():
|
| 30 |
+
return {
|
| 31 |
+
"status": "error",
|
| 32 |
+
"message": "Failed to import hf_unified_server.app",
|
| 33 |
+
"detail": str(_import_error),
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
@app.get("/")
|
| 37 |
def root():
|
| 38 |
+
return {
|
| 39 |
+
"message": "Crypto backend failed to load. "
|
| 40 |
+
"Check logs and dependencies. See /health for details."
|
| 41 |
+
}
|
| 42 |
|
|
|
|
| 43 |
__all__ = ["app"]
|
package-lock.json
CHANGED
|
@@ -8,13 +8,390 @@
|
|
| 8 |
"name": "crypto-api-resource-monitor",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"license": "MIT",
|
|
|
|
|
|
|
|
|
|
| 11 |
"devDependencies": {
|
| 12 |
-
"fast-check": "^3.15.0"
|
|
|
|
| 13 |
},
|
| 14 |
"engines": {
|
| 15 |
"node": ">=14.0.0"
|
| 16 |
}
|
| 17 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"node_modules/fast-check": {
|
| 19 |
"version": "3.23.2",
|
| 20 |
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
|
@@ -38,6 +415,319 @@
|
|
| 38 |
"node": ">=8.0.0"
|
| 39 |
}
|
| 40 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
"node_modules/pure-rand": {
|
| 42 |
"version": "6.1.0",
|
| 43 |
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
|
@@ -54,6 +744,223 @@
|
|
| 54 |
}
|
| 55 |
],
|
| 56 |
"license": "MIT"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
}
|
|
|
|
| 8 |
"name": "crypto-api-resource-monitor",
|
| 9 |
"version": "1.0.0",
|
| 10 |
"license": "MIT",
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"charmap": "^1.1.6"
|
| 13 |
+
},
|
| 14 |
"devDependencies": {
|
| 15 |
+
"fast-check": "^3.15.0",
|
| 16 |
+
"jsdom": "^23.0.0"
|
| 17 |
},
|
| 18 |
"engines": {
|
| 19 |
"node": ">=14.0.0"
|
| 20 |
}
|
| 21 |
},
|
| 22 |
+
"node_modules/@asamuzakjp/css-color": {
|
| 23 |
+
"version": "3.2.0",
|
| 24 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
| 25 |
+
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
| 26 |
+
"dev": true,
|
| 27 |
+
"license": "MIT",
|
| 28 |
+
"dependencies": {
|
| 29 |
+
"@csstools/css-calc": "^2.1.3",
|
| 30 |
+
"@csstools/css-color-parser": "^3.0.9",
|
| 31 |
+
"@csstools/css-parser-algorithms": "^3.0.4",
|
| 32 |
+
"@csstools/css-tokenizer": "^3.0.3",
|
| 33 |
+
"lru-cache": "^10.4.3"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"node_modules/@asamuzakjp/dom-selector": {
|
| 37 |
+
"version": "2.0.2",
|
| 38 |
+
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-2.0.2.tgz",
|
| 39 |
+
"integrity": "sha512-x1KXOatwofR6ZAYzXRBL5wrdV0vwNxlTCK9NCuLqAzQYARqGcvFwiJA6A1ERuh+dgeA4Dxm3JBYictIes+SqUQ==",
|
| 40 |
+
"dev": true,
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"dependencies": {
|
| 43 |
+
"bidi-js": "^1.0.3",
|
| 44 |
+
"css-tree": "^2.3.1",
|
| 45 |
+
"is-potential-custom-element-name": "^1.0.1"
|
| 46 |
+
}
|
| 47 |
+
},
|
| 48 |
+
"node_modules/@csstools/color-helpers": {
|
| 49 |
+
"version": "5.1.0",
|
| 50 |
+
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
| 51 |
+
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
| 52 |
+
"dev": true,
|
| 53 |
+
"funding": [
|
| 54 |
+
{
|
| 55 |
+
"type": "github",
|
| 56 |
+
"url": "https://github.com/sponsors/csstools"
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"type": "opencollective",
|
| 60 |
+
"url": "https://opencollective.com/csstools"
|
| 61 |
+
}
|
| 62 |
+
],
|
| 63 |
+
"license": "MIT-0",
|
| 64 |
+
"engines": {
|
| 65 |
+
"node": ">=18"
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"node_modules/@csstools/css-calc": {
|
| 69 |
+
"version": "2.1.4",
|
| 70 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
| 71 |
+
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
| 72 |
+
"dev": true,
|
| 73 |
+
"funding": [
|
| 74 |
+
{
|
| 75 |
+
"type": "github",
|
| 76 |
+
"url": "https://github.com/sponsors/csstools"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"type": "opencollective",
|
| 80 |
+
"url": "https://opencollective.com/csstools"
|
| 81 |
+
}
|
| 82 |
+
],
|
| 83 |
+
"license": "MIT",
|
| 84 |
+
"engines": {
|
| 85 |
+
"node": ">=18"
|
| 86 |
+
},
|
| 87 |
+
"peerDependencies": {
|
| 88 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 89 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"node_modules/@csstools/css-color-parser": {
|
| 93 |
+
"version": "3.1.0",
|
| 94 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
| 95 |
+
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
| 96 |
+
"dev": true,
|
| 97 |
+
"funding": [
|
| 98 |
+
{
|
| 99 |
+
"type": "github",
|
| 100 |
+
"url": "https://github.com/sponsors/csstools"
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"type": "opencollective",
|
| 104 |
+
"url": "https://opencollective.com/csstools"
|
| 105 |
+
}
|
| 106 |
+
],
|
| 107 |
+
"license": "MIT",
|
| 108 |
+
"dependencies": {
|
| 109 |
+
"@csstools/color-helpers": "^5.1.0",
|
| 110 |
+
"@csstools/css-calc": "^2.1.4"
|
| 111 |
+
},
|
| 112 |
+
"engines": {
|
| 113 |
+
"node": ">=18"
|
| 114 |
+
},
|
| 115 |
+
"peerDependencies": {
|
| 116 |
+
"@csstools/css-parser-algorithms": "^3.0.5",
|
| 117 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"node_modules/@csstools/css-parser-algorithms": {
|
| 121 |
+
"version": "3.0.5",
|
| 122 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
| 123 |
+
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
| 124 |
+
"dev": true,
|
| 125 |
+
"funding": [
|
| 126 |
+
{
|
| 127 |
+
"type": "github",
|
| 128 |
+
"url": "https://github.com/sponsors/csstools"
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"type": "opencollective",
|
| 132 |
+
"url": "https://opencollective.com/csstools"
|
| 133 |
+
}
|
| 134 |
+
],
|
| 135 |
+
"license": "MIT",
|
| 136 |
+
"peer": true,
|
| 137 |
+
"engines": {
|
| 138 |
+
"node": ">=18"
|
| 139 |
+
},
|
| 140 |
+
"peerDependencies": {
|
| 141 |
+
"@csstools/css-tokenizer": "^3.0.4"
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"node_modules/@csstools/css-tokenizer": {
|
| 145 |
+
"version": "3.0.4",
|
| 146 |
+
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
| 147 |
+
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
| 148 |
+
"dev": true,
|
| 149 |
+
"funding": [
|
| 150 |
+
{
|
| 151 |
+
"type": "github",
|
| 152 |
+
"url": "https://github.com/sponsors/csstools"
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"type": "opencollective",
|
| 156 |
+
"url": "https://opencollective.com/csstools"
|
| 157 |
+
}
|
| 158 |
+
],
|
| 159 |
+
"license": "MIT",
|
| 160 |
+
"peer": true,
|
| 161 |
+
"engines": {
|
| 162 |
+
"node": ">=18"
|
| 163 |
+
}
|
| 164 |
+
},
|
| 165 |
+
"node_modules/agent-base": {
|
| 166 |
+
"version": "7.1.4",
|
| 167 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 168 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 169 |
+
"dev": true,
|
| 170 |
+
"license": "MIT",
|
| 171 |
+
"engines": {
|
| 172 |
+
"node": ">= 14"
|
| 173 |
+
}
|
| 174 |
+
},
|
| 175 |
+
"node_modules/asynckit": {
|
| 176 |
+
"version": "0.4.0",
|
| 177 |
+
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
| 178 |
+
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
| 179 |
+
"dev": true,
|
| 180 |
+
"license": "MIT"
|
| 181 |
+
},
|
| 182 |
+
"node_modules/bidi-js": {
|
| 183 |
+
"version": "1.0.3",
|
| 184 |
+
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
| 185 |
+
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
| 186 |
+
"dev": true,
|
| 187 |
+
"license": "MIT",
|
| 188 |
+
"dependencies": {
|
| 189 |
+
"require-from-string": "^2.0.2"
|
| 190 |
+
}
|
| 191 |
+
},
|
| 192 |
+
"node_modules/call-bind-apply-helpers": {
|
| 193 |
+
"version": "1.0.2",
|
| 194 |
+
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
| 195 |
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
| 196 |
+
"dev": true,
|
| 197 |
+
"license": "MIT",
|
| 198 |
+
"dependencies": {
|
| 199 |
+
"es-errors": "^1.3.0",
|
| 200 |
+
"function-bind": "^1.1.2"
|
| 201 |
+
},
|
| 202 |
+
"engines": {
|
| 203 |
+
"node": ">= 0.4"
|
| 204 |
+
}
|
| 205 |
+
},
|
| 206 |
+
"node_modules/charmap": {
|
| 207 |
+
"version": "1.1.6",
|
| 208 |
+
"resolved": "https://registry.npmjs.org/charmap/-/charmap-1.1.6.tgz",
|
| 209 |
+
"integrity": "sha512-BfgDyIZOETYrvthjHHLY44S3s21o/VRZoLBSbJbbMs/k2XluBvdayklV4BBs4tB0MgiUgAPRWoOkYeBLk58R1w==",
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"dependencies": {
|
| 212 |
+
"es6-object-assign": "^1.1.0"
|
| 213 |
+
}
|
| 214 |
+
},
|
| 215 |
+
"node_modules/combined-stream": {
|
| 216 |
+
"version": "1.0.8",
|
| 217 |
+
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
| 218 |
+
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
| 219 |
+
"dev": true,
|
| 220 |
+
"license": "MIT",
|
| 221 |
+
"dependencies": {
|
| 222 |
+
"delayed-stream": "~1.0.0"
|
| 223 |
+
},
|
| 224 |
+
"engines": {
|
| 225 |
+
"node": ">= 0.8"
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
"node_modules/css-tree": {
|
| 229 |
+
"version": "2.3.1",
|
| 230 |
+
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
| 231 |
+
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
| 232 |
+
"dev": true,
|
| 233 |
+
"license": "MIT",
|
| 234 |
+
"dependencies": {
|
| 235 |
+
"mdn-data": "2.0.30",
|
| 236 |
+
"source-map-js": "^1.0.1"
|
| 237 |
+
},
|
| 238 |
+
"engines": {
|
| 239 |
+
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
| 240 |
+
}
|
| 241 |
+
},
|
| 242 |
+
"node_modules/cssstyle": {
|
| 243 |
+
"version": "4.6.0",
|
| 244 |
+
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
| 245 |
+
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
| 246 |
+
"dev": true,
|
| 247 |
+
"license": "MIT",
|
| 248 |
+
"dependencies": {
|
| 249 |
+
"@asamuzakjp/css-color": "^3.2.0",
|
| 250 |
+
"rrweb-cssom": "^0.8.0"
|
| 251 |
+
},
|
| 252 |
+
"engines": {
|
| 253 |
+
"node": ">=18"
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
"node_modules/cssstyle/node_modules/rrweb-cssom": {
|
| 257 |
+
"version": "0.8.0",
|
| 258 |
+
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
| 259 |
+
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
| 260 |
+
"dev": true,
|
| 261 |
+
"license": "MIT"
|
| 262 |
+
},
|
| 263 |
+
"node_modules/data-urls": {
|
| 264 |
+
"version": "5.0.0",
|
| 265 |
+
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
| 266 |
+
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
| 267 |
+
"dev": true,
|
| 268 |
+
"license": "MIT",
|
| 269 |
+
"dependencies": {
|
| 270 |
+
"whatwg-mimetype": "^4.0.0",
|
| 271 |
+
"whatwg-url": "^14.0.0"
|
| 272 |
+
},
|
| 273 |
+
"engines": {
|
| 274 |
+
"node": ">=18"
|
| 275 |
+
}
|
| 276 |
+
},
|
| 277 |
+
"node_modules/debug": {
|
| 278 |
+
"version": "4.4.3",
|
| 279 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 280 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 281 |
+
"dev": true,
|
| 282 |
+
"license": "MIT",
|
| 283 |
+
"dependencies": {
|
| 284 |
+
"ms": "^2.1.3"
|
| 285 |
+
},
|
| 286 |
+
"engines": {
|
| 287 |
+
"node": ">=6.0"
|
| 288 |
+
},
|
| 289 |
+
"peerDependenciesMeta": {
|
| 290 |
+
"supports-color": {
|
| 291 |
+
"optional": true
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
},
|
| 295 |
+
"node_modules/decimal.js": {
|
| 296 |
+
"version": "10.6.0",
|
| 297 |
+
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
| 298 |
+
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
| 299 |
+
"dev": true,
|
| 300 |
+
"license": "MIT"
|
| 301 |
+
},
|
| 302 |
+
"node_modules/delayed-stream": {
|
| 303 |
+
"version": "1.0.0",
|
| 304 |
+
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
| 305 |
+
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
| 306 |
+
"dev": true,
|
| 307 |
+
"license": "MIT",
|
| 308 |
+
"engines": {
|
| 309 |
+
"node": ">=0.4.0"
|
| 310 |
+
}
|
| 311 |
+
},
|
| 312 |
+
"node_modules/dunder-proto": {
|
| 313 |
+
"version": "1.0.1",
|
| 314 |
+
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
| 315 |
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
| 316 |
+
"dev": true,
|
| 317 |
+
"license": "MIT",
|
| 318 |
+
"dependencies": {
|
| 319 |
+
"call-bind-apply-helpers": "^1.0.1",
|
| 320 |
+
"es-errors": "^1.3.0",
|
| 321 |
+
"gopd": "^1.2.0"
|
| 322 |
+
},
|
| 323 |
+
"engines": {
|
| 324 |
+
"node": ">= 0.4"
|
| 325 |
+
}
|
| 326 |
+
},
|
| 327 |
+
"node_modules/entities": {
|
| 328 |
+
"version": "6.0.1",
|
| 329 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
| 330 |
+
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
| 331 |
+
"dev": true,
|
| 332 |
+
"license": "BSD-2-Clause",
|
| 333 |
+
"engines": {
|
| 334 |
+
"node": ">=0.12"
|
| 335 |
+
},
|
| 336 |
+
"funding": {
|
| 337 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
"node_modules/es-define-property": {
|
| 341 |
+
"version": "1.0.1",
|
| 342 |
+
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
| 343 |
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
| 344 |
+
"dev": true,
|
| 345 |
+
"license": "MIT",
|
| 346 |
+
"engines": {
|
| 347 |
+
"node": ">= 0.4"
|
| 348 |
+
}
|
| 349 |
+
},
|
| 350 |
+
"node_modules/es-errors": {
|
| 351 |
+
"version": "1.3.0",
|
| 352 |
+
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
| 353 |
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
| 354 |
+
"dev": true,
|
| 355 |
+
"license": "MIT",
|
| 356 |
+
"engines": {
|
| 357 |
+
"node": ">= 0.4"
|
| 358 |
+
}
|
| 359 |
+
},
|
| 360 |
+
"node_modules/es-object-atoms": {
|
| 361 |
+
"version": "1.1.1",
|
| 362 |
+
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
| 363 |
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
| 364 |
+
"dev": true,
|
| 365 |
+
"license": "MIT",
|
| 366 |
+
"dependencies": {
|
| 367 |
+
"es-errors": "^1.3.0"
|
| 368 |
+
},
|
| 369 |
+
"engines": {
|
| 370 |
+
"node": ">= 0.4"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/es-set-tostringtag": {
|
| 374 |
+
"version": "2.1.0",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
| 376 |
+
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
| 377 |
+
"dev": true,
|
| 378 |
+
"license": "MIT",
|
| 379 |
+
"dependencies": {
|
| 380 |
+
"es-errors": "^1.3.0",
|
| 381 |
+
"get-intrinsic": "^1.2.6",
|
| 382 |
+
"has-tostringtag": "^1.0.2",
|
| 383 |
+
"hasown": "^2.0.2"
|
| 384 |
+
},
|
| 385 |
+
"engines": {
|
| 386 |
+
"node": ">= 0.4"
|
| 387 |
+
}
|
| 388 |
+
},
|
| 389 |
+
"node_modules/es6-object-assign": {
|
| 390 |
+
"version": "1.1.0",
|
| 391 |
+
"resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz",
|
| 392 |
+
"integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==",
|
| 393 |
+
"license": "MIT"
|
| 394 |
+
},
|
| 395 |
"node_modules/fast-check": {
|
| 396 |
"version": "3.23.2",
|
| 397 |
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
|
|
|
|
| 415 |
"node": ">=8.0.0"
|
| 416 |
}
|
| 417 |
},
|
| 418 |
+
"node_modules/form-data": {
|
| 419 |
+
"version": "4.0.5",
|
| 420 |
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
| 421 |
+
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
| 422 |
+
"dev": true,
|
| 423 |
+
"license": "MIT",
|
| 424 |
+
"dependencies": {
|
| 425 |
+
"asynckit": "^0.4.0",
|
| 426 |
+
"combined-stream": "^1.0.8",
|
| 427 |
+
"es-set-tostringtag": "^2.1.0",
|
| 428 |
+
"hasown": "^2.0.2",
|
| 429 |
+
"mime-types": "^2.1.12"
|
| 430 |
+
},
|
| 431 |
+
"engines": {
|
| 432 |
+
"node": ">= 6"
|
| 433 |
+
}
|
| 434 |
+
},
|
| 435 |
+
"node_modules/function-bind": {
|
| 436 |
+
"version": "1.1.2",
|
| 437 |
+
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
| 438 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 439 |
+
"dev": true,
|
| 440 |
+
"license": "MIT",
|
| 441 |
+
"funding": {
|
| 442 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 443 |
+
}
|
| 444 |
+
},
|
| 445 |
+
"node_modules/get-intrinsic": {
|
| 446 |
+
"version": "1.3.0",
|
| 447 |
+
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
| 448 |
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
| 449 |
+
"dev": true,
|
| 450 |
+
"license": "MIT",
|
| 451 |
+
"dependencies": {
|
| 452 |
+
"call-bind-apply-helpers": "^1.0.2",
|
| 453 |
+
"es-define-property": "^1.0.1",
|
| 454 |
+
"es-errors": "^1.3.0",
|
| 455 |
+
"es-object-atoms": "^1.1.1",
|
| 456 |
+
"function-bind": "^1.1.2",
|
| 457 |
+
"get-proto": "^1.0.1",
|
| 458 |
+
"gopd": "^1.2.0",
|
| 459 |
+
"has-symbols": "^1.1.0",
|
| 460 |
+
"hasown": "^2.0.2",
|
| 461 |
+
"math-intrinsics": "^1.1.0"
|
| 462 |
+
},
|
| 463 |
+
"engines": {
|
| 464 |
+
"node": ">= 0.4"
|
| 465 |
+
},
|
| 466 |
+
"funding": {
|
| 467 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 468 |
+
}
|
| 469 |
+
},
|
| 470 |
+
"node_modules/get-proto": {
|
| 471 |
+
"version": "1.0.1",
|
| 472 |
+
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
| 473 |
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
| 474 |
+
"dev": true,
|
| 475 |
+
"license": "MIT",
|
| 476 |
+
"dependencies": {
|
| 477 |
+
"dunder-proto": "^1.0.1",
|
| 478 |
+
"es-object-atoms": "^1.0.0"
|
| 479 |
+
},
|
| 480 |
+
"engines": {
|
| 481 |
+
"node": ">= 0.4"
|
| 482 |
+
}
|
| 483 |
+
},
|
| 484 |
+
"node_modules/gopd": {
|
| 485 |
+
"version": "1.2.0",
|
| 486 |
+
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
| 487 |
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
| 488 |
+
"dev": true,
|
| 489 |
+
"license": "MIT",
|
| 490 |
+
"engines": {
|
| 491 |
+
"node": ">= 0.4"
|
| 492 |
+
},
|
| 493 |
+
"funding": {
|
| 494 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 495 |
+
}
|
| 496 |
+
},
|
| 497 |
+
"node_modules/has-symbols": {
|
| 498 |
+
"version": "1.1.0",
|
| 499 |
+
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
| 500 |
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
| 501 |
+
"dev": true,
|
| 502 |
+
"license": "MIT",
|
| 503 |
+
"engines": {
|
| 504 |
+
"node": ">= 0.4"
|
| 505 |
+
},
|
| 506 |
+
"funding": {
|
| 507 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 508 |
+
}
|
| 509 |
+
},
|
| 510 |
+
"node_modules/has-tostringtag": {
|
| 511 |
+
"version": "1.0.2",
|
| 512 |
+
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
| 513 |
+
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
| 514 |
+
"dev": true,
|
| 515 |
+
"license": "MIT",
|
| 516 |
+
"dependencies": {
|
| 517 |
+
"has-symbols": "^1.0.3"
|
| 518 |
+
},
|
| 519 |
+
"engines": {
|
| 520 |
+
"node": ">= 0.4"
|
| 521 |
+
},
|
| 522 |
+
"funding": {
|
| 523 |
+
"url": "https://github.com/sponsors/ljharb"
|
| 524 |
+
}
|
| 525 |
+
},
|
| 526 |
+
"node_modules/hasown": {
|
| 527 |
+
"version": "2.0.2",
|
| 528 |
+
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
| 529 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 530 |
+
"dev": true,
|
| 531 |
+
"license": "MIT",
|
| 532 |
+
"dependencies": {
|
| 533 |
+
"function-bind": "^1.1.2"
|
| 534 |
+
},
|
| 535 |
+
"engines": {
|
| 536 |
+
"node": ">= 0.4"
|
| 537 |
+
}
|
| 538 |
+
},
|
| 539 |
+
"node_modules/html-encoding-sniffer": {
|
| 540 |
+
"version": "4.0.0",
|
| 541 |
+
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
| 542 |
+
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
| 543 |
+
"dev": true,
|
| 544 |
+
"license": "MIT",
|
| 545 |
+
"dependencies": {
|
| 546 |
+
"whatwg-encoding": "^3.1.1"
|
| 547 |
+
},
|
| 548 |
+
"engines": {
|
| 549 |
+
"node": ">=18"
|
| 550 |
+
}
|
| 551 |
+
},
|
| 552 |
+
"node_modules/http-proxy-agent": {
|
| 553 |
+
"version": "7.0.2",
|
| 554 |
+
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
| 555 |
+
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
| 556 |
+
"dev": true,
|
| 557 |
+
"license": "MIT",
|
| 558 |
+
"dependencies": {
|
| 559 |
+
"agent-base": "^7.1.0",
|
| 560 |
+
"debug": "^4.3.4"
|
| 561 |
+
},
|
| 562 |
+
"engines": {
|
| 563 |
+
"node": ">= 14"
|
| 564 |
+
}
|
| 565 |
+
},
|
| 566 |
+
"node_modules/https-proxy-agent": {
|
| 567 |
+
"version": "7.0.6",
|
| 568 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 569 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 570 |
+
"dev": true,
|
| 571 |
+
"license": "MIT",
|
| 572 |
+
"dependencies": {
|
| 573 |
+
"agent-base": "^7.1.2",
|
| 574 |
+
"debug": "4"
|
| 575 |
+
},
|
| 576 |
+
"engines": {
|
| 577 |
+
"node": ">= 14"
|
| 578 |
+
}
|
| 579 |
+
},
|
| 580 |
+
"node_modules/iconv-lite": {
|
| 581 |
+
"version": "0.6.3",
|
| 582 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
| 583 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
| 584 |
+
"dev": true,
|
| 585 |
+
"license": "MIT",
|
| 586 |
+
"dependencies": {
|
| 587 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
| 588 |
+
},
|
| 589 |
+
"engines": {
|
| 590 |
+
"node": ">=0.10.0"
|
| 591 |
+
}
|
| 592 |
+
},
|
| 593 |
+
"node_modules/is-potential-custom-element-name": {
|
| 594 |
+
"version": "1.0.1",
|
| 595 |
+
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
| 596 |
+
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
| 597 |
+
"dev": true,
|
| 598 |
+
"license": "MIT"
|
| 599 |
+
},
|
| 600 |
+
"node_modules/jsdom": {
|
| 601 |
+
"version": "23.2.0",
|
| 602 |
+
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-23.2.0.tgz",
|
| 603 |
+
"integrity": "sha512-L88oL7D/8ufIES+Zjz7v0aes+oBMh2Xnh3ygWvL0OaICOomKEPKuPnIfBJekiXr+BHbbMjrWn/xqrDQuxFTeyA==",
|
| 604 |
+
"dev": true,
|
| 605 |
+
"license": "MIT",
|
| 606 |
+
"dependencies": {
|
| 607 |
+
"@asamuzakjp/dom-selector": "^2.0.1",
|
| 608 |
+
"cssstyle": "^4.0.1",
|
| 609 |
+
"data-urls": "^5.0.0",
|
| 610 |
+
"decimal.js": "^10.4.3",
|
| 611 |
+
"form-data": "^4.0.0",
|
| 612 |
+
"html-encoding-sniffer": "^4.0.0",
|
| 613 |
+
"http-proxy-agent": "^7.0.0",
|
| 614 |
+
"https-proxy-agent": "^7.0.2",
|
| 615 |
+
"is-potential-custom-element-name": "^1.0.1",
|
| 616 |
+
"parse5": "^7.1.2",
|
| 617 |
+
"rrweb-cssom": "^0.6.0",
|
| 618 |
+
"saxes": "^6.0.0",
|
| 619 |
+
"symbol-tree": "^3.2.4",
|
| 620 |
+
"tough-cookie": "^4.1.3",
|
| 621 |
+
"w3c-xmlserializer": "^5.0.0",
|
| 622 |
+
"webidl-conversions": "^7.0.0",
|
| 623 |
+
"whatwg-encoding": "^3.1.1",
|
| 624 |
+
"whatwg-mimetype": "^4.0.0",
|
| 625 |
+
"whatwg-url": "^14.0.0",
|
| 626 |
+
"ws": "^8.16.0",
|
| 627 |
+
"xml-name-validator": "^5.0.0"
|
| 628 |
+
},
|
| 629 |
+
"engines": {
|
| 630 |
+
"node": ">=18"
|
| 631 |
+
},
|
| 632 |
+
"peerDependencies": {
|
| 633 |
+
"canvas": "^2.11.2"
|
| 634 |
+
},
|
| 635 |
+
"peerDependenciesMeta": {
|
| 636 |
+
"canvas": {
|
| 637 |
+
"optional": true
|
| 638 |
+
}
|
| 639 |
+
}
|
| 640 |
+
},
|
| 641 |
+
"node_modules/lru-cache": {
|
| 642 |
+
"version": "10.4.3",
|
| 643 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
| 644 |
+
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
| 645 |
+
"dev": true,
|
| 646 |
+
"license": "ISC"
|
| 647 |
+
},
|
| 648 |
+
"node_modules/math-intrinsics": {
|
| 649 |
+
"version": "1.1.0",
|
| 650 |
+
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
| 651 |
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
| 652 |
+
"dev": true,
|
| 653 |
+
"license": "MIT",
|
| 654 |
+
"engines": {
|
| 655 |
+
"node": ">= 0.4"
|
| 656 |
+
}
|
| 657 |
+
},
|
| 658 |
+
"node_modules/mdn-data": {
|
| 659 |
+
"version": "2.0.30",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
| 661 |
+
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
| 662 |
+
"dev": true,
|
| 663 |
+
"license": "CC0-1.0"
|
| 664 |
+
},
|
| 665 |
+
"node_modules/mime-db": {
|
| 666 |
+
"version": "1.52.0",
|
| 667 |
+
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
| 668 |
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
| 669 |
+
"dev": true,
|
| 670 |
+
"license": "MIT",
|
| 671 |
+
"engines": {
|
| 672 |
+
"node": ">= 0.6"
|
| 673 |
+
}
|
| 674 |
+
},
|
| 675 |
+
"node_modules/mime-types": {
|
| 676 |
+
"version": "2.1.35",
|
| 677 |
+
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
| 678 |
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
| 679 |
+
"dev": true,
|
| 680 |
+
"license": "MIT",
|
| 681 |
+
"dependencies": {
|
| 682 |
+
"mime-db": "1.52.0"
|
| 683 |
+
},
|
| 684 |
+
"engines": {
|
| 685 |
+
"node": ">= 0.6"
|
| 686 |
+
}
|
| 687 |
+
},
|
| 688 |
+
"node_modules/ms": {
|
| 689 |
+
"version": "2.1.3",
|
| 690 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 691 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 692 |
+
"dev": true,
|
| 693 |
+
"license": "MIT"
|
| 694 |
+
},
|
| 695 |
+
"node_modules/parse5": {
|
| 696 |
+
"version": "7.3.0",
|
| 697 |
+
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
| 698 |
+
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
| 699 |
+
"dev": true,
|
| 700 |
+
"license": "MIT",
|
| 701 |
+
"dependencies": {
|
| 702 |
+
"entities": "^6.0.0"
|
| 703 |
+
},
|
| 704 |
+
"funding": {
|
| 705 |
+
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
| 706 |
+
}
|
| 707 |
+
},
|
| 708 |
+
"node_modules/psl": {
|
| 709 |
+
"version": "1.15.0",
|
| 710 |
+
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
| 711 |
+
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
| 712 |
+
"dev": true,
|
| 713 |
+
"license": "MIT",
|
| 714 |
+
"dependencies": {
|
| 715 |
+
"punycode": "^2.3.1"
|
| 716 |
+
},
|
| 717 |
+
"funding": {
|
| 718 |
+
"url": "https://github.com/sponsors/lupomontero"
|
| 719 |
+
}
|
| 720 |
+
},
|
| 721 |
+
"node_modules/punycode": {
|
| 722 |
+
"version": "2.3.1",
|
| 723 |
+
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
| 724 |
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
| 725 |
+
"dev": true,
|
| 726 |
+
"license": "MIT",
|
| 727 |
+
"engines": {
|
| 728 |
+
"node": ">=6"
|
| 729 |
+
}
|
| 730 |
+
},
|
| 731 |
"node_modules/pure-rand": {
|
| 732 |
"version": "6.1.0",
|
| 733 |
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
|
|
|
|
| 744 |
}
|
| 745 |
],
|
| 746 |
"license": "MIT"
|
| 747 |
+
},
|
| 748 |
+
"node_modules/querystringify": {
|
| 749 |
+
"version": "2.2.0",
|
| 750 |
+
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
| 751 |
+
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
| 752 |
+
"dev": true,
|
| 753 |
+
"license": "MIT"
|
| 754 |
+
},
|
| 755 |
+
"node_modules/require-from-string": {
|
| 756 |
+
"version": "2.0.2",
|
| 757 |
+
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
| 758 |
+
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
| 759 |
+
"dev": true,
|
| 760 |
+
"license": "MIT",
|
| 761 |
+
"engines": {
|
| 762 |
+
"node": ">=0.10.0"
|
| 763 |
+
}
|
| 764 |
+
},
|
| 765 |
+
"node_modules/requires-port": {
|
| 766 |
+
"version": "1.0.0",
|
| 767 |
+
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
| 768 |
+
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
| 769 |
+
"dev": true,
|
| 770 |
+
"license": "MIT"
|
| 771 |
+
},
|
| 772 |
+
"node_modules/rrweb-cssom": {
|
| 773 |
+
"version": "0.6.0",
|
| 774 |
+
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz",
|
| 775 |
+
"integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==",
|
| 776 |
+
"dev": true,
|
| 777 |
+
"license": "MIT"
|
| 778 |
+
},
|
| 779 |
+
"node_modules/safer-buffer": {
|
| 780 |
+
"version": "2.1.2",
|
| 781 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
| 782 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
| 783 |
+
"dev": true,
|
| 784 |
+
"license": "MIT"
|
| 785 |
+
},
|
| 786 |
+
"node_modules/saxes": {
|
| 787 |
+
"version": "6.0.0",
|
| 788 |
+
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
| 789 |
+
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
| 790 |
+
"dev": true,
|
| 791 |
+
"license": "ISC",
|
| 792 |
+
"dependencies": {
|
| 793 |
+
"xmlchars": "^2.2.0"
|
| 794 |
+
},
|
| 795 |
+
"engines": {
|
| 796 |
+
"node": ">=v12.22.7"
|
| 797 |
+
}
|
| 798 |
+
},
|
| 799 |
+
"node_modules/source-map-js": {
|
| 800 |
+
"version": "1.2.1",
|
| 801 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 802 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 803 |
+
"dev": true,
|
| 804 |
+
"license": "BSD-3-Clause",
|
| 805 |
+
"engines": {
|
| 806 |
+
"node": ">=0.10.0"
|
| 807 |
+
}
|
| 808 |
+
},
|
| 809 |
+
"node_modules/symbol-tree": {
|
| 810 |
+
"version": "3.2.4",
|
| 811 |
+
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
| 812 |
+
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
| 813 |
+
"dev": true,
|
| 814 |
+
"license": "MIT"
|
| 815 |
+
},
|
| 816 |
+
"node_modules/tough-cookie": {
|
| 817 |
+
"version": "4.1.4",
|
| 818 |
+
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
| 819 |
+
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
| 820 |
+
"dev": true,
|
| 821 |
+
"license": "BSD-3-Clause",
|
| 822 |
+
"dependencies": {
|
| 823 |
+
"psl": "^1.1.33",
|
| 824 |
+
"punycode": "^2.1.1",
|
| 825 |
+
"universalify": "^0.2.0",
|
| 826 |
+
"url-parse": "^1.5.3"
|
| 827 |
+
},
|
| 828 |
+
"engines": {
|
| 829 |
+
"node": ">=6"
|
| 830 |
+
}
|
| 831 |
+
},
|
| 832 |
+
"node_modules/tr46": {
|
| 833 |
+
"version": "5.1.1",
|
| 834 |
+
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
| 835 |
+
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
| 836 |
+
"dev": true,
|
| 837 |
+
"license": "MIT",
|
| 838 |
+
"dependencies": {
|
| 839 |
+
"punycode": "^2.3.1"
|
| 840 |
+
},
|
| 841 |
+
"engines": {
|
| 842 |
+
"node": ">=18"
|
| 843 |
+
}
|
| 844 |
+
},
|
| 845 |
+
"node_modules/universalify": {
|
| 846 |
+
"version": "0.2.0",
|
| 847 |
+
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
| 848 |
+
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
| 849 |
+
"dev": true,
|
| 850 |
+
"license": "MIT",
|
| 851 |
+
"engines": {
|
| 852 |
+
"node": ">= 4.0.0"
|
| 853 |
+
}
|
| 854 |
+
},
|
| 855 |
+
"node_modules/url-parse": {
|
| 856 |
+
"version": "1.5.10",
|
| 857 |
+
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
| 858 |
+
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
| 859 |
+
"dev": true,
|
| 860 |
+
"license": "MIT",
|
| 861 |
+
"dependencies": {
|
| 862 |
+
"querystringify": "^2.1.1",
|
| 863 |
+
"requires-port": "^1.0.0"
|
| 864 |
+
}
|
| 865 |
+
},
|
| 866 |
+
"node_modules/w3c-xmlserializer": {
|
| 867 |
+
"version": "5.0.0",
|
| 868 |
+
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
| 869 |
+
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
| 870 |
+
"dev": true,
|
| 871 |
+
"license": "MIT",
|
| 872 |
+
"dependencies": {
|
| 873 |
+
"xml-name-validator": "^5.0.0"
|
| 874 |
+
},
|
| 875 |
+
"engines": {
|
| 876 |
+
"node": ">=18"
|
| 877 |
+
}
|
| 878 |
+
},
|
| 879 |
+
"node_modules/webidl-conversions": {
|
| 880 |
+
"version": "7.0.0",
|
| 881 |
+
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
| 882 |
+
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
| 883 |
+
"dev": true,
|
| 884 |
+
"license": "BSD-2-Clause",
|
| 885 |
+
"engines": {
|
| 886 |
+
"node": ">=12"
|
| 887 |
+
}
|
| 888 |
+
},
|
| 889 |
+
"node_modules/whatwg-encoding": {
|
| 890 |
+
"version": "3.1.1",
|
| 891 |
+
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
| 892 |
+
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
| 893 |
+
"dev": true,
|
| 894 |
+
"license": "MIT",
|
| 895 |
+
"dependencies": {
|
| 896 |
+
"iconv-lite": "0.6.3"
|
| 897 |
+
},
|
| 898 |
+
"engines": {
|
| 899 |
+
"node": ">=18"
|
| 900 |
+
}
|
| 901 |
+
},
|
| 902 |
+
"node_modules/whatwg-mimetype": {
|
| 903 |
+
"version": "4.0.0",
|
| 904 |
+
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
| 905 |
+
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
| 906 |
+
"dev": true,
|
| 907 |
+
"license": "MIT",
|
| 908 |
+
"engines": {
|
| 909 |
+
"node": ">=18"
|
| 910 |
+
}
|
| 911 |
+
},
|
| 912 |
+
"node_modules/whatwg-url": {
|
| 913 |
+
"version": "14.2.0",
|
| 914 |
+
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
| 915 |
+
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
| 916 |
+
"dev": true,
|
| 917 |
+
"license": "MIT",
|
| 918 |
+
"dependencies": {
|
| 919 |
+
"tr46": "^5.1.0",
|
| 920 |
+
"webidl-conversions": "^7.0.0"
|
| 921 |
+
},
|
| 922 |
+
"engines": {
|
| 923 |
+
"node": ">=18"
|
| 924 |
+
}
|
| 925 |
+
},
|
| 926 |
+
"node_modules/ws": {
|
| 927 |
+
"version": "8.18.3",
|
| 928 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
| 929 |
+
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
| 930 |
+
"dev": true,
|
| 931 |
+
"license": "MIT",
|
| 932 |
+
"engines": {
|
| 933 |
+
"node": ">=10.0.0"
|
| 934 |
+
},
|
| 935 |
+
"peerDependencies": {
|
| 936 |
+
"bufferutil": "^4.0.1",
|
| 937 |
+
"utf-8-validate": ">=5.0.2"
|
| 938 |
+
},
|
| 939 |
+
"peerDependenciesMeta": {
|
| 940 |
+
"bufferutil": {
|
| 941 |
+
"optional": true
|
| 942 |
+
},
|
| 943 |
+
"utf-8-validate": {
|
| 944 |
+
"optional": true
|
| 945 |
+
}
|
| 946 |
+
}
|
| 947 |
+
},
|
| 948 |
+
"node_modules/xml-name-validator": {
|
| 949 |
+
"version": "5.0.0",
|
| 950 |
+
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
| 951 |
+
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
| 952 |
+
"dev": true,
|
| 953 |
+
"license": "Apache-2.0",
|
| 954 |
+
"engines": {
|
| 955 |
+
"node": ">=18"
|
| 956 |
+
}
|
| 957 |
+
},
|
| 958 |
+
"node_modules/xmlchars": {
|
| 959 |
+
"version": "2.2.0",
|
| 960 |
+
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
| 961 |
+
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
| 962 |
+
"dev": true,
|
| 963 |
+
"license": "MIT"
|
| 964 |
}
|
| 965 |
}
|
| 966 |
}
|
package.json
CHANGED
|
@@ -10,7 +10,16 @@
|
|
| 10 |
"dashboard": "python3 -m http.server 8080",
|
| 11 |
"full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
|
| 12 |
"test:free-resources": "node free_resources_selftest.mjs",
|
| 13 |
-
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
},
|
| 15 |
"keywords": [
|
| 16 |
"cryptocurrency",
|
|
@@ -32,5 +41,8 @@
|
|
| 32 |
"repository": {
|
| 33 |
"type": "git",
|
| 34 |
"url": "https://github.com/nimazasinich/crypto-dt-source.git"
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
}
|
|
|
|
| 10 |
"dashboard": "python3 -m http.server 8080",
|
| 11 |
"full-check": "node api-monitor.js && node failover-manager.js && echo 'Open http://localhost:8080/dashboard.html in your browser' && python3 -m http.server 8080",
|
| 12 |
"test:free-resources": "node free_resources_selftest.mjs",
|
| 13 |
+
"test:free-resources:win": "powershell -NoProfile -ExecutionPolicy Bypass -File test_free_endpoints.ps1",
|
| 14 |
+
"test:theme": "node tests/verify_theme.js",
|
| 15 |
+
"test:api-client": "node tests/test_apiClient.test.js",
|
| 16 |
+
"test:ui-feedback": "node tests/test_ui_feedback.test.js",
|
| 17 |
+
"test:fallback": "pytest tests/test_fallback_service.py -m fallback",
|
| 18 |
+
"test:api-health": "pytest tests/test_fallback_service.py -m api_health"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"fast-check": "^3.15.0",
|
| 22 |
+
"jsdom": "^23.0.0"
|
| 23 |
},
|
| 24 |
"keywords": [
|
| 25 |
"cryptocurrency",
|
|
|
|
| 41 |
"repository": {
|
| 42 |
"type": "git",
|
| 43 |
"url": "https://github.com/nimazasinich/crypto-dt-source.git"
|
| 44 |
+
},
|
| 45 |
+
"dependencies": {
|
| 46 |
+
"charmap": "^1.1.6"
|
| 47 |
}
|
| 48 |
}
|
production_server.py
CHANGED
|
@@ -441,7 +441,7 @@ async def remove_custom_api(name: str):
|
|
| 441 |
# Serve static files
|
| 442 |
@app.get("/")
|
| 443 |
async def root():
|
| 444 |
-
return FileResponse("
|
| 445 |
|
| 446 |
@app.get("/index.html")
|
| 447 |
async def index():
|
|
|
|
| 441 |
# Serve static files
|
| 442 |
@app.get("/")
|
| 443 |
async def root():
|
| 444 |
+
return FileResponse("admin.html")
|
| 445 |
|
| 446 |
@app.get("/index.html")
|
| 447 |
async def index():
|
provider_validator.py
CHANGED
|
@@ -278,7 +278,13 @@ class ProviderValidator:
|
|
| 278 |
try:
|
| 279 |
start = time.time()
|
| 280 |
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
response = await client.get(f"https://huggingface.co/api/models/{model_id}")
|
| 283 |
elapsed_ms = (time.time() - start) * 1000
|
| 284 |
|
|
|
|
| 278 |
try:
|
| 279 |
start = time.time()
|
| 280 |
|
| 281 |
+
# Get HF token from environment or use default
|
| 282 |
+
hf_token = os.getenv("HF_TOKEN") or "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"
|
| 283 |
+
headers = {}
|
| 284 |
+
if hf_token:
|
| 285 |
+
headers["Authorization"] = f"Bearer {hf_token}"
|
| 286 |
+
|
| 287 |
+
async with httpx.AsyncClient(timeout=self.timeout, headers=headers) as client:
|
| 288 |
response = await client.get(f"https://huggingface.co/api/models/{model_id}")
|
| 289 |
elapsed_ms = (time.time() - start) * 1000
|
| 290 |
|
pytest.ini
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
markers =
|
| 3 |
+
fallback: Tests validating the canonical fallback registry integration.
|
| 4 |
+
api_health: Tests covering API health/failover scenarios.
|
real_server.py
CHANGED
|
@@ -380,7 +380,7 @@ async def api_config_keys():
|
|
| 380 |
# Serve static files
|
| 381 |
@app.get("/")
|
| 382 |
async def root():
|
| 383 |
-
return FileResponse("
|
| 384 |
|
| 385 |
@app.get("/dashboard.html")
|
| 386 |
async def dashboard():
|
|
|
|
| 380 |
# Serve static files
|
| 381 |
@app.get("/")
|
| 382 |
async def root():
|
| 383 |
+
return FileResponse("admin.html")
|
| 384 |
|
| 385 |
@app.get("/dashboard.html")
|
| 386 |
async def dashboard():
|