Upload 54 files
Browse files- api/endpoints.py +168 -164
- api/trading/decision/api_server_extended.py +122 -0
- static/css/main.css +411 -4
- static/js/app.js +163 -1
api/endpoints.py
CHANGED
|
@@ -38,85 +38,87 @@ class TestKeyRequest(BaseModel):
|
|
| 38 |
|
| 39 |
# ============================================================================
|
| 40 |
# GET /api/status - System Overview
|
|
|
|
|
|
|
| 41 |
# ============================================================================
|
| 42 |
|
| 43 |
-
@router.get("/status")
|
| 44 |
-
async def get_system_status():
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
|
| 121 |
|
| 122 |
# ============================================================================
|
|
@@ -203,95 +205,97 @@ async def get_categories():
|
|
| 203 |
|
| 204 |
# ============================================================================
|
| 205 |
# GET /api/providers - Provider List with Filters
|
|
|
|
|
|
|
| 206 |
# ============================================================================
|
| 207 |
|
| 208 |
-
@router.get("/providers")
|
| 209 |
-
async def get_providers(
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
):
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
|
| 296 |
|
| 297 |
# ============================================================================
|
|
|
|
| 38 |
|
| 39 |
# ============================================================================
|
| 40 |
# GET /api/status - System Overview
|
| 41 |
+
# NOTE: This route is disabled to avoid conflict with api_server_extended.py
|
| 42 |
+
# The status endpoint is handled directly in api_server_extended.py
|
| 43 |
# ============================================================================
|
| 44 |
|
| 45 |
+
# @router.get("/status")
|
| 46 |
+
# async def get_system_status():
|
| 47 |
+
# """
|
| 48 |
+
# Get comprehensive system status overview
|
| 49 |
+
#
|
| 50 |
+
# Returns:
|
| 51 |
+
# System overview with provider counts, health metrics, and last update
|
| 52 |
+
# """
|
| 53 |
+
# try:
|
| 54 |
+
# # Get latest system metrics from database
|
| 55 |
+
# latest_metrics = db_manager.get_latest_system_metrics()
|
| 56 |
+
#
|
| 57 |
+
# if latest_metrics:
|
| 58 |
+
# return {
|
| 59 |
+
# "total_apis": latest_metrics.total_providers,
|
| 60 |
+
# "online": latest_metrics.online_count,
|
| 61 |
+
# "degraded": latest_metrics.degraded_count,
|
| 62 |
+
# "offline": latest_metrics.offline_count,
|
| 63 |
+
# "avg_response_time_ms": round(latest_metrics.avg_response_time_ms, 2),
|
| 64 |
+
# "last_update": latest_metrics.timestamp.isoformat(),
|
| 65 |
+
# "system_health": latest_metrics.system_health
|
| 66 |
+
# }
|
| 67 |
+
#
|
| 68 |
+
# # Fallback: Calculate from providers if no metrics available
|
| 69 |
+
# providers = db_manager.get_all_providers()
|
| 70 |
+
#
|
| 71 |
+
# # Get recent connection attempts for each provider
|
| 72 |
+
# status_counts = {"online": 0, "degraded": 0, "offline": 0}
|
| 73 |
+
# response_times = []
|
| 74 |
+
#
|
| 75 |
+
# for provider in providers:
|
| 76 |
+
# attempts = db_manager.get_connection_attempts(
|
| 77 |
+
# provider_id=provider.id,
|
| 78 |
+
# hours=1,
|
| 79 |
+
# limit=10
|
| 80 |
+
# )
|
| 81 |
+
#
|
| 82 |
+
# if attempts:
|
| 83 |
+
# recent = attempts[0]
|
| 84 |
+
# if recent.status == "success" and recent.response_time_ms and recent.response_time_ms < 2000:
|
| 85 |
+
# status_counts["online"] += 1
|
| 86 |
+
# response_times.append(recent.response_time_ms)
|
| 87 |
+
# elif recent.status == "success":
|
| 88 |
+
# status_counts["degraded"] += 1
|
| 89 |
+
# if recent.response_time_ms:
|
| 90 |
+
# response_times.append(recent.response_time_ms)
|
| 91 |
+
# else:
|
| 92 |
+
# status_counts["offline"] += 1
|
| 93 |
+
# else:
|
| 94 |
+
# status_counts["offline"] += 1
|
| 95 |
+
#
|
| 96 |
+
# avg_response_time = sum(response_times) / len(response_times) if response_times else 0
|
| 97 |
+
#
|
| 98 |
+
# # Determine system health
|
| 99 |
+
# total = len(providers)
|
| 100 |
+
# online_pct = (status_counts["online"] / total * 100) if total > 0 else 0
|
| 101 |
+
#
|
| 102 |
+
# if online_pct >= 90:
|
| 103 |
+
# system_health = "healthy"
|
| 104 |
+
# elif online_pct >= 70:
|
| 105 |
+
# system_health = "degraded"
|
| 106 |
+
# else:
|
| 107 |
+
# system_health = "unhealthy"
|
| 108 |
+
#
|
| 109 |
+
# return {
|
| 110 |
+
# "total_apis": total,
|
| 111 |
+
# "online": status_counts["online"],
|
| 112 |
+
# "degraded": status_counts["degraded"],
|
| 113 |
+
# "offline": status_counts["offline"],
|
| 114 |
+
# "avg_response_time_ms": round(avg_response_time, 2),
|
| 115 |
+
# "last_update": datetime.utcnow().isoformat(),
|
| 116 |
+
# "system_health": system_health
|
| 117 |
+
# }
|
| 118 |
+
#
|
| 119 |
+
# except Exception as e:
|
| 120 |
+
# logger.error(f"Error getting system status: {e}", exc_info=True)
|
| 121 |
+
# raise HTTPException(status_code=500, detail=f"Failed to get system status: {str(e)}")
|
| 122 |
|
| 123 |
|
| 124 |
# ============================================================================
|
|
|
|
| 205 |
|
| 206 |
# ============================================================================
|
| 207 |
# GET /api/providers - Provider List with Filters
|
| 208 |
+
# NOTE: This route is disabled to avoid conflict with api_server_extended.py
|
| 209 |
+
# The providers endpoint is handled directly in api_server_extended.py
|
| 210 |
# ============================================================================
|
| 211 |
|
| 212 |
+
# @router.get("/providers")
|
| 213 |
+
# async def get_providers(
|
| 214 |
+
# category: Optional[str] = Query(None, description="Filter by category"),
|
| 215 |
+
# status: Optional[str] = Query(None, description="Filter by status (online/degraded/offline)"),
|
| 216 |
+
# search: Optional[str] = Query(None, description="Search by provider name")
|
| 217 |
+
# ):
|
| 218 |
+
# """
|
| 219 |
+
# Get list of providers with optional filtering
|
| 220 |
+
#
|
| 221 |
+
# Args:
|
| 222 |
+
# category: Filter by provider category
|
| 223 |
+
# status: Filter by provider status
|
| 224 |
+
# search: Search by provider name
|
| 225 |
+
#
|
| 226 |
+
# Returns:
|
| 227 |
+
# List of providers with detailed information
|
| 228 |
+
# """
|
| 229 |
+
# try:
|
| 230 |
+
# # Get providers from database
|
| 231 |
+
# providers = db_manager.get_all_providers(category=category)
|
| 232 |
+
#
|
| 233 |
+
# result = []
|
| 234 |
+
#
|
| 235 |
+
# for provider in providers:
|
| 236 |
+
# # Apply search filter
|
| 237 |
+
# if search and search.lower() not in provider.name.lower():
|
| 238 |
+
# continue
|
| 239 |
+
#
|
| 240 |
+
# # Get recent connection attempts
|
| 241 |
+
# attempts = db_manager.get_connection_attempts(
|
| 242 |
+
# provider_id=provider.id,
|
| 243 |
+
# hours=1,
|
| 244 |
+
# limit=10
|
| 245 |
+
# )
|
| 246 |
+
#
|
| 247 |
+
# # Determine provider status
|
| 248 |
+
# provider_status = "offline"
|
| 249 |
+
# response_time_ms = 0
|
| 250 |
+
# last_fetch = None
|
| 251 |
+
#
|
| 252 |
+
# if attempts:
|
| 253 |
+
# recent = attempts[0]
|
| 254 |
+
# last_fetch = recent.timestamp
|
| 255 |
+
#
|
| 256 |
+
# if recent.status == "success":
|
| 257 |
+
# if recent.response_time_ms and recent.response_time_ms < 2000:
|
| 258 |
+
# provider_status = "online"
|
| 259 |
+
# else:
|
| 260 |
+
# provider_status = "degraded"
|
| 261 |
+
# response_time_ms = recent.response_time_ms or 0
|
| 262 |
+
# elif recent.status == "rate_limited":
|
| 263 |
+
# provider_status = "degraded"
|
| 264 |
+
# else:
|
| 265 |
+
# provider_status = "offline"
|
| 266 |
+
#
|
| 267 |
+
# # Apply status filter
|
| 268 |
+
# if status and provider_status != status:
|
| 269 |
+
# continue
|
| 270 |
+
#
|
| 271 |
+
# # Get rate limit info
|
| 272 |
+
# rate_limit_status = rate_limiter.get_status(provider.name)
|
| 273 |
+
# rate_limit = None
|
| 274 |
+
# if rate_limit_status:
|
| 275 |
+
# rate_limit = f"{rate_limit_status['current_usage']}/{rate_limit_status['limit_value']} {rate_limit_status['limit_type']}"
|
| 276 |
+
# elif provider.rate_limit_type and provider.rate_limit_value:
|
| 277 |
+
# rate_limit = f"0/{provider.rate_limit_value} {provider.rate_limit_type}"
|
| 278 |
+
#
|
| 279 |
+
# # Get schedule config
|
| 280 |
+
# schedule_config = db_manager.get_schedule_config(provider.id)
|
| 281 |
+
#
|
| 282 |
+
# result.append({
|
| 283 |
+
# "id": provider.id,
|
| 284 |
+
# "name": provider.name,
|
| 285 |
+
# "category": provider.category,
|
| 286 |
+
# "status": provider_status,
|
| 287 |
+
# "response_time_ms": response_time_ms,
|
| 288 |
+
# "rate_limit": rate_limit,
|
| 289 |
+
# "last_fetch": last_fetch.isoformat() if last_fetch else None,
|
| 290 |
+
# "has_key": provider.requires_key,
|
| 291 |
+
# "endpoints": provider.endpoint_url
|
| 292 |
+
# })
|
| 293 |
+
#
|
| 294 |
+
# return result
|
| 295 |
+
#
|
| 296 |
+
# except Exception as e:
|
| 297 |
+
# logger.error(f"Error getting providers: {e}", exc_info=True)
|
| 298 |
+
# raise HTTPException(status_code=500, detail=f"Failed to get providers: {str(e)}")
|
| 299 |
|
| 300 |
|
| 301 |
# ============================================================================
|
api/trading/decision/api_server_extended.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@app.post("/api/trading/decision")
|
| 2 |
+
async def trading_decision(request: Dict[str, Any]):
|
| 3 |
+
"""
|
| 4 |
+
FIXED: Get trading decision based on sentiment classification.
|
| 5 |
+
Uses ElKulako/cryptobert (classification model) instead of generation.
|
| 6 |
+
|
| 7 |
+
Logic:
|
| 8 |
+
- BULLISH/POSITIVE label -> BUY
|
| 9 |
+
- BEARISH/NEGATIVE label -> SELL
|
| 10 |
+
- NEUTRAL or error -> HOLD
|
| 11 |
+
|
| 12 |
+
Always returns valid JSON (never crashes with 500).
|
| 13 |
+
"""
|
| 14 |
+
try:
|
| 15 |
+
symbol = request.get("symbol", "").strip().upper()
|
| 16 |
+
context = request.get("context", "").strip()
|
| 17 |
+
|
| 18 |
+
if not symbol:
|
| 19 |
+
raise HTTPException(status_code=400, detail="Symbol is required")
|
| 20 |
+
|
| 21 |
+
# Build analysis text
|
| 22 |
+
if context:
|
| 23 |
+
analysis_text = f"{symbol} {context}"
|
| 24 |
+
else:
|
| 25 |
+
analysis_text = f"{symbol} market analysis"
|
| 26 |
+
|
| 27 |
+
# Default safe response
|
| 28 |
+
default_response = {
|
| 29 |
+
"success": True,
|
| 30 |
+
"available": False,
|
| 31 |
+
"decision": "HOLD",
|
| 32 |
+
"confidence": 0.5,
|
| 33 |
+
"rationale": "Unable to analyze sentiment - defaulting to HOLD for safety",
|
| 34 |
+
"symbol": symbol,
|
| 35 |
+
"model": "fallback",
|
| 36 |
+
"context_provided": bool(context),
|
| 37 |
+
"timestamp": datetime.now().isoformat()
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from ai_models import _registry, MODEL_SPECS, ModelNotAvailable
|
| 42 |
+
|
| 43 |
+
# Try to use the trading model (crypto_trading_lm -> ElKulako/cryptobert)
|
| 44 |
+
trading_key = "crypto_trading_lm"
|
| 45 |
+
|
| 46 |
+
if trading_key not in MODEL_SPECS:
|
| 47 |
+
logger.warning("Trading model key not found in MODEL_SPECS")
|
| 48 |
+
return default_response
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
# Get the classification pipeline (lazy loaded)
|
| 52 |
+
pipe = _registry.get_pipeline(trading_key)
|
| 53 |
+
spec = MODEL_SPECS[trading_key]
|
| 54 |
+
|
| 55 |
+
# Run classification
|
| 56 |
+
result = pipe(analysis_text[:512])
|
| 57 |
+
if isinstance(result, list) and result:
|
| 58 |
+
result = result[0]
|
| 59 |
+
|
| 60 |
+
label = result.get("label", "NEUTRAL").upper()
|
| 61 |
+
score = result.get("score", 0.5)
|
| 62 |
+
|
| 63 |
+
# FIXED LOGIC: Map label to trading decision
|
| 64 |
+
decision = "HOLD" # Default
|
| 65 |
+
if "BULLISH" in label or "POSITIVE" in label or "LABEL_2" in label:
|
| 66 |
+
decision = "BUY"
|
| 67 |
+
elif "BEARISH" in label or "NEGATIVE" in label or "LABEL_0" in label:
|
| 68 |
+
decision = "SELL"
|
| 69 |
+
else:
|
| 70 |
+
decision = "HOLD"
|
| 71 |
+
|
| 72 |
+
# Build rationale
|
| 73 |
+
sentiment_word = "bullish" if decision == "BUY" else ("bearish" if decision == "SELL" else "neutral")
|
| 74 |
+
rationale = f"Model detected {sentiment_word} sentiment (label: {label}, confidence: {score:.2f})"
|
| 75 |
+
if context:
|
| 76 |
+
rationale += f" based on: {context[:200]}"
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"success": True,
|
| 80 |
+
"available": True,
|
| 81 |
+
"decision": decision,
|
| 82 |
+
"confidence": float(score),
|
| 83 |
+
"rationale": rationale,
|
| 84 |
+
"symbol": symbol,
|
| 85 |
+
"model": spec.model_id,
|
| 86 |
+
"sentiment": sentiment_word,
|
| 87 |
+
"raw_label": label,
|
| 88 |
+
"context_provided": bool(context),
|
| 89 |
+
"timestamp": datetime.now().isoformat()
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
except ModelNotAvailable as e:
|
| 93 |
+
logger.warning(f"Trading model not available: {e}")
|
| 94 |
+
default_response["error"] = f"Model unavailable: {str(e)[:100]}"
|
| 95 |
+
default_response["note"] = "Model in cooldown or failed to load"
|
| 96 |
+
return default_response
|
| 97 |
+
|
| 98 |
+
except ImportError:
|
| 99 |
+
logger.error("ai_models module not available")
|
| 100 |
+
default_response["error"] = "AI models module not available"
|
| 101 |
+
return default_response
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.warning(f"Sentiment analysis failed: {e}")
|
| 104 |
+
default_response["error"] = f"Analysis failed: {str(e)[:100]}"
|
| 105 |
+
default_response["note"] = "Using default HOLD signal due to analysis failure"
|
| 106 |
+
return default_response
|
| 107 |
+
|
| 108 |
+
except HTTPException:
|
| 109 |
+
raise
|
| 110 |
+
except Exception as e:
|
| 111 |
+
logger.error(f"Trading decision endpoint error: {e}")
|
| 112 |
+
# Never crash - always return valid JSON
|
| 113 |
+
return {
|
| 114 |
+
"success": True,
|
| 115 |
+
"available": False,
|
| 116 |
+
"error": f"Endpoint error: {str(e)[:100]}",
|
| 117 |
+
"decision": "HOLD",
|
| 118 |
+
"confidence": 0.5,
|
| 119 |
+
"rationale": "Error occurred during analysis - defaulting to HOLD for safety",
|
| 120 |
+
"symbol": request.get("symbol", "UNKNOWN"),
|
| 121 |
+
"timestamp": datetime.now().isoformat()
|
| 122 |
+
}
|
static/css/main.css
CHANGED
|
@@ -125,6 +125,12 @@ body::before {
|
|
| 125 |
display: flex;
|
| 126 |
align-items: center;
|
| 127 |
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
.sidebar-logo .logo-icon {
|
|
@@ -475,12 +481,14 @@ body::before {
|
|
| 475 |
margin-bottom: 30px;
|
| 476 |
}
|
| 477 |
|
| 478 |
-
|
|
|
|
| 479 |
.stats-grid {
|
| 480 |
grid-template-columns: repeat(2, 1fr);
|
| 481 |
}
|
| 482 |
}
|
| 483 |
|
|
|
|
| 484 |
@media (max-width: 768px) {
|
| 485 |
.stats-grid {
|
| 486 |
grid-template-columns: 1fr;
|
|
@@ -971,7 +979,7 @@ table tr:hover {
|
|
| 971 |
}
|
| 972 |
|
| 973 |
.stats-grid {
|
| 974 |
-
grid-template-columns: repeat(
|
| 975 |
gap: 25px;
|
| 976 |
}
|
| 977 |
|
|
@@ -1144,7 +1152,7 @@ table tr:hover {
|
|
| 1144 |
}
|
| 1145 |
|
| 1146 |
.stats-grid {
|
| 1147 |
-
grid-template-columns: repeat(
|
| 1148 |
gap: 30px;
|
| 1149 |
margin-bottom: 40px;
|
| 1150 |
}
|
|
@@ -1326,7 +1334,7 @@ table tr:hover {
|
|
| 1326 |
}
|
| 1327 |
|
| 1328 |
.stats-grid {
|
| 1329 |
-
grid-template-columns: repeat(
|
| 1330 |
gap: 35px;
|
| 1331 |
margin-bottom: 50px;
|
| 1332 |
}
|
|
@@ -1845,6 +1853,405 @@ body.light-theme ::-webkit-scrollbar-track {
|
|
| 1845 |
|
| 1846 |
.sentiment-gauge-container {
|
| 1847 |
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1848 |
max-width: 300px;
|
| 1849 |
}
|
| 1850 |
}
|
|
|
|
| 125 |
display: flex;
|
| 126 |
align-items: center;
|
| 127 |
gap: 12px;
|
| 128 |
+
transition: var(--transition-normal);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.sidebar-logo:hover {
|
| 132 |
+
opacity: 0.9;
|
| 133 |
+
transform: scale(1.02);
|
| 134 |
}
|
| 135 |
|
| 136 |
.sidebar-logo .logo-icon {
|
|
|
|
| 481 |
margin-bottom: 30px;
|
| 482 |
}
|
| 483 |
|
| 484 |
+
/* Tablet: 2 columns */
|
| 485 |
+
@media (max-width: 1024px) and (min-width: 769px) {
|
| 486 |
.stats-grid {
|
| 487 |
grid-template-columns: repeat(2, 1fr);
|
| 488 |
}
|
| 489 |
}
|
| 490 |
|
| 491 |
+
/* Mobile: 1 column */
|
| 492 |
@media (max-width: 768px) {
|
| 493 |
.stats-grid {
|
| 494 |
grid-template-columns: 1fr;
|
|
|
|
| 979 |
}
|
| 980 |
|
| 981 |
.stats-grid {
|
| 982 |
+
grid-template-columns: repeat(4, 1fr);
|
| 983 |
gap: 25px;
|
| 984 |
}
|
| 985 |
|
|
|
|
| 1152 |
}
|
| 1153 |
|
| 1154 |
.stats-grid {
|
| 1155 |
+
grid-template-columns: repeat(4, 1fr);
|
| 1156 |
gap: 30px;
|
| 1157 |
margin-bottom: 40px;
|
| 1158 |
}
|
|
|
|
| 1334 |
}
|
| 1335 |
|
| 1336 |
.stats-grid {
|
| 1337 |
+
grid-template-columns: repeat(4, 1fr);
|
| 1338 |
gap: 35px;
|
| 1339 |
margin-bottom: 50px;
|
| 1340 |
}
|
|
|
|
| 1853 |
|
| 1854 |
.sentiment-gauge-container {
|
| 1855 |
width: 100%;
|
| 1856 |
+
}
|
| 1857 |
+
|
| 1858 |
+
/* ===== DIAGNOSTICS STYLES ===== */
|
| 1859 |
+
|
| 1860 |
+
/* Navigation Sections */
|
| 1861 |
+
.nav-section {
|
| 1862 |
+
margin-bottom: 8px;
|
| 1863 |
+
}
|
| 1864 |
+
|
| 1865 |
+
.nav-section-header {
|
| 1866 |
+
display: flex;
|
| 1867 |
+
align-items: center;
|
| 1868 |
+
gap: 12px;
|
| 1869 |
+
padding: 12px 16px;
|
| 1870 |
+
color: var(--text-secondary);
|
| 1871 |
+
font-size: 14px;
|
| 1872 |
+
font-weight: 600;
|
| 1873 |
+
text-transform: uppercase;
|
| 1874 |
+
letter-spacing: 0.5px;
|
| 1875 |
+
opacity: 0.8;
|
| 1876 |
+
}
|
| 1877 |
+
|
| 1878 |
+
.nav-section-header svg {
|
| 1879 |
+
opacity: 0.7;
|
| 1880 |
+
}
|
| 1881 |
+
|
| 1882 |
+
.nav-section-items {
|
| 1883 |
+
display: flex;
|
| 1884 |
+
flex-direction: column;
|
| 1885 |
+
gap: 2px;
|
| 1886 |
+
margin-left: 8px;
|
| 1887 |
+
}
|
| 1888 |
+
|
| 1889 |
+
.nav-subitem {
|
| 1890 |
+
padding-left: 32px !important;
|
| 1891 |
+
font-size: 14px !important;
|
| 1892 |
+
opacity: 0.9;
|
| 1893 |
+
}
|
| 1894 |
+
|
| 1895 |
+
.nav-subitem:hover {
|
| 1896 |
+
opacity: 1;
|
| 1897 |
+
}
|
| 1898 |
+
|
| 1899 |
+
/* Diagnostic Header */
|
| 1900 |
+
.diagnostic-header {
|
| 1901 |
+
margin-bottom: 24px;
|
| 1902 |
+
}
|
| 1903 |
+
|
| 1904 |
+
.diagnostic-title h2 {
|
| 1905 |
+
color: var(--text-primary);
|
| 1906 |
+
font-size: 28px;
|
| 1907 |
+
font-weight: 700;
|
| 1908 |
+
margin: 0 0 8px 0;
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
.diagnostic-title p {
|
| 1912 |
+
color: var(--text-secondary);
|
| 1913 |
+
font-size: 16px;
|
| 1914 |
+
margin: 0;
|
| 1915 |
+
}
|
| 1916 |
+
|
| 1917 |
+
/* Status Cards Grid */
|
| 1918 |
+
.status-cards-grid {
|
| 1919 |
+
display: grid;
|
| 1920 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 1921 |
+
gap: 16px;
|
| 1922 |
+
margin-bottom: 32px;
|
| 1923 |
+
}
|
| 1924 |
+
|
| 1925 |
+
.status-card {
|
| 1926 |
+
background: var(--dark-card);
|
| 1927 |
+
border: 1px solid var(--border);
|
| 1928 |
+
border-radius: 12px;
|
| 1929 |
+
padding: 20px;
|
| 1930 |
+
display: flex;
|
| 1931 |
+
align-items: center;
|
| 1932 |
+
gap: 16px;
|
| 1933 |
+
transition: var(--transition-normal);
|
| 1934 |
+
}
|
| 1935 |
+
|
| 1936 |
+
.status-card:hover {
|
| 1937 |
+
border-color: var(--primary);
|
| 1938 |
+
box-shadow: var(--glow);
|
| 1939 |
+
}
|
| 1940 |
+
|
| 1941 |
+
.status-icon {
|
| 1942 |
+
font-size: 32px;
|
| 1943 |
+
opacity: 0.8;
|
| 1944 |
+
}
|
| 1945 |
+
|
| 1946 |
+
.status-content {
|
| 1947 |
+
flex: 1;
|
| 1948 |
+
}
|
| 1949 |
+
|
| 1950 |
+
.status-label {
|
| 1951 |
+
color: var(--text-secondary);
|
| 1952 |
+
font-size: 14px;
|
| 1953 |
+
margin-bottom: 4px;
|
| 1954 |
+
}
|
| 1955 |
+
|
| 1956 |
+
.status-value {
|
| 1957 |
+
color: var(--text-primary);
|
| 1958 |
+
font-size: 18px;
|
| 1959 |
+
font-weight: 600;
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
/* Diagnostic Actions */
|
| 1963 |
+
.diagnostic-actions {
|
| 1964 |
+
display: flex;
|
| 1965 |
+
gap: 12px;
|
| 1966 |
+
margin-bottom: 32px;
|
| 1967 |
+
flex-wrap: wrap;
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
+
.diagnostic-actions .btn-primary {
|
| 1971 |
+
display: flex;
|
| 1972 |
+
align-items: center;
|
| 1973 |
+
justify-content: center;
|
| 1974 |
+
gap: 8px;
|
| 1975 |
+
padding: 12px 24px;
|
| 1976 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
| 1977 |
+
border: none;
|
| 1978 |
+
border-radius: 10px;
|
| 1979 |
+
color: white;
|
| 1980 |
+
font-weight: 600;
|
| 1981 |
+
font-size: 14px;
|
| 1982 |
+
cursor: pointer;
|
| 1983 |
+
transition: all var(--transition-fast);
|
| 1984 |
+
position: relative;
|
| 1985 |
+
overflow: hidden;
|
| 1986 |
+
}
|
| 1987 |
+
|
| 1988 |
+
.diagnostic-actions .btn-primary::before {
|
| 1989 |
+
content: '';
|
| 1990 |
+
position: absolute;
|
| 1991 |
+
top: 50%;
|
| 1992 |
+
left: 50%;
|
| 1993 |
+
width: 0;
|
| 1994 |
+
height: 0;
|
| 1995 |
+
border-radius: 50%;
|
| 1996 |
+
background: rgba(255, 255, 255, 0.2);
|
| 1997 |
+
transform: translate(-50%, -50%);
|
| 1998 |
+
transition: width 0.6s, height 0.6s;
|
| 1999 |
+
}
|
| 2000 |
+
|
| 2001 |
+
.diagnostic-actions .btn-primary:hover:not(:disabled) {
|
| 2002 |
+
transform: translateY(-2px);
|
| 2003 |
+
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
| 2004 |
+
}
|
| 2005 |
+
|
| 2006 |
+
.diagnostic-actions .btn-primary:hover:not(:disabled)::before {
|
| 2007 |
+
width: 300px;
|
| 2008 |
+
height: 300px;
|
| 2009 |
+
}
|
| 2010 |
+
|
| 2011 |
+
.diagnostic-actions .btn-primary:disabled {
|
| 2012 |
+
opacity: 0.6;
|
| 2013 |
+
cursor: not-allowed;
|
| 2014 |
+
transform: none;
|
| 2015 |
+
}
|
| 2016 |
+
|
| 2017 |
+
.diagnostic-actions .btn-primary:disabled::before {
|
| 2018 |
+
display: none;
|
| 2019 |
+
}
|
| 2020 |
+
|
| 2021 |
+
.diagnostic-actions .btn-primary svg {
|
| 2022 |
+
width: 16px;
|
| 2023 |
+
height: 16px;
|
| 2024 |
+
stroke-width: 2;
|
| 2025 |
+
flex-shrink: 0;
|
| 2026 |
+
}
|
| 2027 |
+
|
| 2028 |
+
.diagnostic-actions .btn-secondary {
|
| 2029 |
+
display: flex;
|
| 2030 |
+
align-items: center;
|
| 2031 |
+
justify-content: center;
|
| 2032 |
+
gap: 8px;
|
| 2033 |
+
padding: 12px 24px;
|
| 2034 |
+
background: rgba(102, 126, 234, 0.15);
|
| 2035 |
+
border: 1px solid var(--primary);
|
| 2036 |
+
border-radius: 10px;
|
| 2037 |
+
color: var(--text-primary);
|
| 2038 |
+
font-weight: 600;
|
| 2039 |
+
font-size: 14px;
|
| 2040 |
+
cursor: pointer;
|
| 2041 |
+
transition: all var(--transition-fast);
|
| 2042 |
+
position: relative;
|
| 2043 |
+
overflow: hidden;
|
| 2044 |
+
}
|
| 2045 |
+
|
| 2046 |
+
.diagnostic-actions .btn-secondary::before {
|
| 2047 |
+
content: '';
|
| 2048 |
+
position: absolute;
|
| 2049 |
+
top: 50%;
|
| 2050 |
+
left: 50%;
|
| 2051 |
+
width: 0;
|
| 2052 |
+
height: 0;
|
| 2053 |
+
border-radius: 50%;
|
| 2054 |
+
background: rgba(102, 126, 234, 0.1);
|
| 2055 |
+
transform: translate(-50%, -50%);
|
| 2056 |
+
transition: width 0.6s, height 0.6s;
|
| 2057 |
+
}
|
| 2058 |
+
|
| 2059 |
+
.diagnostic-actions .btn-secondary:hover {
|
| 2060 |
+
background: rgba(102, 126, 234, 0.25);
|
| 2061 |
+
border-color: var(--primary-light);
|
| 2062 |
+
transform: translateY(-2px);
|
| 2063 |
+
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
| 2064 |
+
}
|
| 2065 |
+
|
| 2066 |
+
.diagnostic-actions .btn-secondary:hover::before {
|
| 2067 |
+
width: 300px;
|
| 2068 |
+
height: 300px;
|
| 2069 |
+
}
|
| 2070 |
+
|
| 2071 |
+
.diagnostic-actions .btn-secondary svg {
|
| 2072 |
+
width: 16px;
|
| 2073 |
+
height: 16px;
|
| 2074 |
+
stroke-width: 2;
|
| 2075 |
+
flex-shrink: 0;
|
| 2076 |
+
}
|
| 2077 |
+
|
| 2078 |
+
/* Test Progress */
|
| 2079 |
+
#test-progress {
|
| 2080 |
+
display: flex;
|
| 2081 |
+
align-items: center;
|
| 2082 |
+
gap: 12px;
|
| 2083 |
+
color: var(--text-secondary);
|
| 2084 |
+
font-size: 14px;
|
| 2085 |
+
}
|
| 2086 |
+
|
| 2087 |
+
.spinner {
|
| 2088 |
+
width: 16px;
|
| 2089 |
+
height: 16px;
|
| 2090 |
+
border: 2px solid var(--border);
|
| 2091 |
+
border-top: 2px solid var(--primary);
|
| 2092 |
+
border-radius: 50%;
|
| 2093 |
+
animation: spin 1s linear infinite;
|
| 2094 |
+
}
|
| 2095 |
+
|
| 2096 |
+
@keyframes spin {
|
| 2097 |
+
0% { transform: rotate(0deg); }
|
| 2098 |
+
100% { transform: rotate(360deg); }
|
| 2099 |
+
}
|
| 2100 |
+
|
| 2101 |
+
/* Diagnostic Output */
|
| 2102 |
+
.diagnostic-output-section {
|
| 2103 |
+
margin-bottom: 32px;
|
| 2104 |
+
}
|
| 2105 |
+
|
| 2106 |
+
.diagnostic-output-container {
|
| 2107 |
+
max-height: 500px;
|
| 2108 |
+
overflow-y: auto;
|
| 2109 |
+
border: 1px solid var(--border);
|
| 2110 |
+
border-radius: 8px;
|
| 2111 |
+
background: var(--dark);
|
| 2112 |
+
}
|
| 2113 |
+
|
| 2114 |
+
.diagnostic-output {
|
| 2115 |
+
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
| 2116 |
+
font-size: 14px;
|
| 2117 |
+
line-height: 1.5;
|
| 2118 |
+
color: var(--text-primary);
|
| 2119 |
+
margin: 0;
|
| 2120 |
+
padding: 16px;
|
| 2121 |
+
white-space: pre-wrap;
|
| 2122 |
+
word-wrap: break-word;
|
| 2123 |
+
background: transparent;
|
| 2124 |
+
border: none;
|
| 2125 |
+
min-height: 300px;
|
| 2126 |
+
}
|
| 2127 |
+
|
| 2128 |
+
/* Diagnostic Summary */
|
| 2129 |
+
.diagnostic-summary {
|
| 2130 |
+
margin-top: 24px;
|
| 2131 |
+
}
|
| 2132 |
+
|
| 2133 |
+
.summary-grid {
|
| 2134 |
+
display: grid;
|
| 2135 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 2136 |
+
gap: 16px;
|
| 2137 |
+
margin-bottom: 16px;
|
| 2138 |
+
}
|
| 2139 |
+
|
| 2140 |
+
.summary-item {
|
| 2141 |
+
display: flex;
|
| 2142 |
+
justify-content: space-between;
|
| 2143 |
+
align-items: center;
|
| 2144 |
+
padding: 12px 16px;
|
| 2145 |
+
background: var(--dark-hover);
|
| 2146 |
+
border-radius: 8px;
|
| 2147 |
+
border: 1px solid var(--border);
|
| 2148 |
+
}
|
| 2149 |
+
|
| 2150 |
+
.summary-label {
|
| 2151 |
+
color: var(--text-secondary);
|
| 2152 |
+
font-weight: 500;
|
| 2153 |
+
}
|
| 2154 |
+
|
| 2155 |
+
.summary-value {
|
| 2156 |
+
color: var(--text-primary);
|
| 2157 |
+
font-weight: 600;
|
| 2158 |
+
}
|
| 2159 |
+
|
| 2160 |
+
/* Responsive Design */
|
| 2161 |
+
@media (max-width: 768px) {
|
| 2162 |
+
.diagnostic-title h2 {
|
| 2163 |
+
font-size: 24px;
|
| 2164 |
+
}
|
| 2165 |
+
|
| 2166 |
+
.status-cards-grid {
|
| 2167 |
+
grid-template-columns: 1fr;
|
| 2168 |
+
}
|
| 2169 |
+
|
| 2170 |
+
.diagnostic-actions {
|
| 2171 |
+
flex-direction: column;
|
| 2172 |
+
}
|
| 2173 |
+
|
| 2174 |
+
.diagnostic-actions .btn-primary,
|
| 2175 |
+
.diagnostic-actions .btn-secondary {
|
| 2176 |
+
width: 100%;
|
| 2177 |
+
justify-content: center;
|
| 2178 |
+
padding: 14px 24px;
|
| 2179 |
+
}
|
| 2180 |
+
|
| 2181 |
+
.summary-grid {
|
| 2182 |
+
grid-template-columns: 1fr;
|
| 2183 |
+
}
|
| 2184 |
+
|
| 2185 |
+
.nav-subitem {
|
| 2186 |
+
padding-left: 24px !important;
|
| 2187 |
+
}
|
| 2188 |
+
}
|
| 2189 |
+
|
| 2190 |
+
@media (max-width: 480px) {
|
| 2191 |
+
.diagnostic-output {
|
| 2192 |
+
font-size: 12px;
|
| 2193 |
+
padding: 12px;
|
| 2194 |
+
}
|
| 2195 |
+
|
| 2196 |
+
.status-card {
|
| 2197 |
+
padding: 16px;
|
| 2198 |
+
flex-direction: column;
|
| 2199 |
+
text-align: center;
|
| 2200 |
+
gap: 12px;
|
| 2201 |
+
}
|
| 2202 |
+
}
|
| 2203 |
+
|
| 2204 |
+
/* ===== PLACEHOLDER PAGE STYLES ===== */
|
| 2205 |
+
|
| 2206 |
+
.placeholder-page {
|
| 2207 |
+
display: flex;
|
| 2208 |
+
flex-direction: column;
|
| 2209 |
+
align-items: center;
|
| 2210 |
+
justify-content: center;
|
| 2211 |
+
min-height: 400px;
|
| 2212 |
+
padding: 60px 20px;
|
| 2213 |
+
text-align: center;
|
| 2214 |
+
}
|
| 2215 |
+
|
| 2216 |
+
.placeholder-icon {
|
| 2217 |
+
font-size: 80px;
|
| 2218 |
+
margin-bottom: 24px;
|
| 2219 |
+
opacity: 0.6;
|
| 2220 |
+
animation: float 3s ease-in-out infinite;
|
| 2221 |
+
}
|
| 2222 |
+
|
| 2223 |
+
@keyframes float {
|
| 2224 |
+
0%, 100% {
|
| 2225 |
+
transform: translateY(0px);
|
| 2226 |
+
}
|
| 2227 |
+
50% {
|
| 2228 |
+
transform: translateY(-10px);
|
| 2229 |
+
}
|
| 2230 |
+
}
|
| 2231 |
+
|
| 2232 |
+
.placeholder-page h2 {
|
| 2233 |
+
color: var(--text-primary);
|
| 2234 |
+
font-size: 32px;
|
| 2235 |
+
font-weight: 700;
|
| 2236 |
+
margin: 0 0 16px 0;
|
| 2237 |
+
}
|
| 2238 |
+
|
| 2239 |
+
.placeholder-page p {
|
| 2240 |
+
color: var(--text-secondary);
|
| 2241 |
+
font-size: 18px;
|
| 2242 |
+
margin: 0 0 8px 0;
|
| 2243 |
+
max-width: 600px;
|
| 2244 |
+
}
|
| 2245 |
+
|
| 2246 |
+
.placeholder-page .text-secondary {
|
| 2247 |
+
color: var(--text-muted);
|
| 2248 |
+
font-size: 14px;
|
| 2249 |
+
margin-bottom: 32px;
|
| 2250 |
+
}
|
| 2251 |
+
|
| 2252 |
+
.placeholder-page .btn-primary {
|
| 2253 |
+
margin-top: 16px;
|
| 2254 |
+
}
|
| 2255 |
max-width: 300px;
|
| 2256 |
}
|
| 2257 |
}
|
static/js/app.js
CHANGED
|
@@ -166,7 +166,12 @@ function switchTab(tabId) {
|
|
| 166 |
'sentiment': { title: 'Sentiment Analysis', subtitle: 'AI-Powered Sentiment Detection' },
|
| 167 |
'trading-assistant': { title: 'Trading Signals', subtitle: 'AI Trading Assistant' },
|
| 168 |
'news': { title: 'Crypto News', subtitle: 'Latest News & Updates' },
|
| 169 |
-
'settings': { title: 'Settings', subtitle: 'System Configuration' }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
};
|
| 171 |
|
| 172 |
const pageTitle = document.getElementById('page-title');
|
|
@@ -1515,4 +1520,161 @@ window.analyzeSentiment = analyzeSentiment;
|
|
| 1515 |
window.runTradingAssistant = runTradingAssistant;
|
| 1516 |
window.formatNumber = formatNumber;
|
| 1517 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1518 |
console.log('β
App.js loaded successfully');
|
|
|
|
| 166 |
'sentiment': { title: 'Sentiment Analysis', subtitle: 'AI-Powered Sentiment Detection' },
|
| 167 |
'trading-assistant': { title: 'Trading Signals', subtitle: 'AI Trading Assistant' },
|
| 168 |
'news': { title: 'Crypto News', subtitle: 'Latest News & Updates' },
|
| 169 |
+
'settings': { title: 'Settings', subtitle: 'System Configuration' },
|
| 170 |
+
'diagnostics': { title: 'Test & Diagnostics', subtitle: 'System Diagnostics & Model Testing' },
|
| 171 |
+
'providers': { title: 'Providers', subtitle: 'Provider Management' },
|
| 172 |
+
'resources': { title: 'Resources', subtitle: 'Resource Management' },
|
| 173 |
+
'defi': { title: 'DeFi Analytics', subtitle: 'DeFi Protocol Analytics' },
|
| 174 |
+
'system-status': { title: 'System Status', subtitle: 'System Health Monitoring' }
|
| 175 |
};
|
| 176 |
|
| 177 |
const pageTitle = document.getElementById('page-title');
|
|
|
|
| 1520 |
window.runTradingAssistant = runTradingAssistant;
|
| 1521 |
window.formatNumber = formatNumber;
|
| 1522 |
|
| 1523 |
+
// ===== DIAGNOSTICS FUNCTIONS =====
|
| 1524 |
+
// Export diagnostic functions to window for onclick handlers
|
| 1525 |
+
|
| 1526 |
+
async function runDiagnostic() {
|
| 1527 |
+
const runBtn = document.getElementById('run-diagnostics-btn');
|
| 1528 |
+
const progressDiv = document.getElementById('test-progress');
|
| 1529 |
+
const outputPre = document.getElementById('diagnostic-output');
|
| 1530 |
+
const summaryDiv = document.getElementById('diagnostic-summary');
|
| 1531 |
+
|
| 1532 |
+
// Disable button and show progress
|
| 1533 |
+
runBtn.disabled = true;
|
| 1534 |
+
runBtn.textContent = 'Running...';
|
| 1535 |
+
progressDiv.style.display = 'block';
|
| 1536 |
+
summaryDiv.style.display = 'none';
|
| 1537 |
+
outputPre.textContent = '';
|
| 1538 |
+
|
| 1539 |
+
try {
|
| 1540 |
+
const response = await fetch('/api/diagnostics/run-test', {
|
| 1541 |
+
method: 'POST',
|
| 1542 |
+
headers: {
|
| 1543 |
+
'Content-Type': 'application/json'
|
| 1544 |
+
}
|
| 1545 |
+
});
|
| 1546 |
+
|
| 1547 |
+
const data = await response.json();
|
| 1548 |
+
|
| 1549 |
+
// Display output with color coding
|
| 1550 |
+
outputPre.innerHTML = colorCodeOutput(data.output);
|
| 1551 |
+
|
| 1552 |
+
// Update summary
|
| 1553 |
+
updateDiagnosticSummary(data);
|
| 1554 |
+
|
| 1555 |
+
// Store last run time
|
| 1556 |
+
localStorage.setItem('lastDiagnosticRun', data.timestamp);
|
| 1557 |
+
|
| 1558 |
+
// Update status cards
|
| 1559 |
+
updateStatusCards(data.summary);
|
| 1560 |
+
|
| 1561 |
+
// Show summary
|
| 1562 |
+
summaryDiv.style.display = 'block';
|
| 1563 |
+
|
| 1564 |
+
// Auto-scroll to bottom
|
| 1565 |
+
outputPre.scrollTop = outputPre.scrollHeight;
|
| 1566 |
+
|
| 1567 |
+
} catch (error) {
|
| 1568 |
+
console.error('Diagnostic error:', error);
|
| 1569 |
+
outputPre.innerHTML = `<span style="color: #ef4444;">β Error running diagnostic: ${error.message}</span>`;
|
| 1570 |
+
showToast('β Diagnostic failed: ' + error.message, 'error');
|
| 1571 |
+
} finally {
|
| 1572 |
+
// Re-enable button
|
| 1573 |
+
runBtn.disabled = false;
|
| 1574 |
+
runBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 6px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>βΆοΈ Run Full Diagnostic';
|
| 1575 |
+
progressDiv.style.display = 'none';
|
| 1576 |
+
}
|
| 1577 |
+
}
|
| 1578 |
+
|
| 1579 |
+
function colorCodeOutput(output) {
|
| 1580 |
+
if (!output) return '';
|
| 1581 |
+
|
| 1582 |
+
return output
|
| 1583 |
+
.replace(/β
/g, '<span style="color: #10b981;">β
</span>')
|
| 1584 |
+
.replace(/β/g, '<span style="color: #ef4444;">β</span>')
|
| 1585 |
+
.replace(/β οΈ/g, '<span style="color: #f59e0b;">β οΈ</span>')
|
| 1586 |
+
.replace(/π/g, '<span style="color: #3b82f6;">π</span>')
|
| 1587 |
+
.replace(/π¦/g, '<span style="color: #8b5cf6;">π¦</span>')
|
| 1588 |
+
.replace(/π/g, '<span style="color: #06b6d4;">π</span>')
|
| 1589 |
+
.replace(/π§ͺ/g, '<span style="color: #84cc16;">π§ͺ</span>')
|
| 1590 |
+
.replace(/π/g, '<span style="color: #f97316;">π</span>')
|
| 1591 |
+
.replace(/π‘/g, '<span style="color: #eab308;">π‘</span>')
|
| 1592 |
+
.replace(/βοΈ/g, '<span style="color: #6b7280;">βοΈ</span>')
|
| 1593 |
+
.split('\n').join('<br>');
|
| 1594 |
+
}
|
| 1595 |
+
|
| 1596 |
+
function updateDiagnosticSummary(data) {
|
| 1597 |
+
document.getElementById('summary-duration').textContent = `${data.duration_seconds}s`;
|
| 1598 |
+
document.getElementById('summary-passed').textContent = data.summary.transformers_available && data.summary.hf_hub_connected ? '2/2' : '1/2';
|
| 1599 |
+
document.getElementById('summary-failed').textContent = (!data.summary.transformers_available || !data.summary.hf_hub_connected) ? '1/2' : '0/2';
|
| 1600 |
+
document.getElementById('summary-critical').textContent = data.summary.critical_issues.length;
|
| 1601 |
+
|
| 1602 |
+
const fixesDiv = document.getElementById('suggested-fixes');
|
| 1603 |
+
if (data.summary.critical_issues.length > 0) {
|
| 1604 |
+
fixesDiv.innerHTML = '<h4>π§ Suggested Fixes:</h4><ul>' +
|
| 1605 |
+
data.summary.critical_issues.map(issue =>
|
| 1606 |
+
`<li>${issue}</li>`
|
| 1607 |
+
).join('') + '</ul>';
|
| 1608 |
+
} else {
|
| 1609 |
+
fixesDiv.innerHTML = '<p style="color: #10b981;">β
No critical issues found</p>';
|
| 1610 |
+
}
|
| 1611 |
+
}
|
| 1612 |
+
|
| 1613 |
+
function updateStatusCards(summary) {
|
| 1614 |
+
document.getElementById('transformers-status-value').innerHTML =
|
| 1615 |
+
summary.transformers_available ?
|
| 1616 |
+
'<span style="color: #10b981;">β
Available</span>' :
|
| 1617 |
+
'<span style="color: #ef4444;">β Not Available</span>';
|
| 1618 |
+
|
| 1619 |
+
document.getElementById('hf-status-value').innerHTML =
|
| 1620 |
+
summary.hf_hub_connected ?
|
| 1621 |
+
'<span style="color: #10b981;">β
Connected</span>' :
|
| 1622 |
+
'<span style="color: #ef4444;">β Disconnected</span>';
|
| 1623 |
+
|
| 1624 |
+
document.getElementById('models-status-value').textContent = summary.models_loaded;
|
| 1625 |
+
|
| 1626 |
+
const lastRun = localStorage.getItem('lastDiagnosticRun');
|
| 1627 |
+
document.getElementById('last-test-value').textContent =
|
| 1628 |
+
lastRun ? new Date(lastRun).toLocaleString() : 'Never';
|
| 1629 |
+
}
|
| 1630 |
+
|
| 1631 |
+
async function refreshDiagnosticStatus() {
|
| 1632 |
+
try {
|
| 1633 |
+
// Get current status from /api/diagnostics/health or similar
|
| 1634 |
+
const response = await fetch('/api/health');
|
| 1635 |
+
const healthData = await response.json();
|
| 1636 |
+
|
| 1637 |
+
// For now, just update the last test time
|
| 1638 |
+
const lastRun = localStorage.getItem('lastDiagnosticRun');
|
| 1639 |
+
document.getElementById('last-test-value').textContent =
|
| 1640 |
+
lastRun ? new Date(lastRun).toLocaleString() : 'Never';
|
| 1641 |
+
|
| 1642 |
+
showToast('β
Status refreshed', 'success');
|
| 1643 |
+
} catch (error) {
|
| 1644 |
+
console.error('Error refreshing status:', error);
|
| 1645 |
+
showToast('β Failed to refresh status', 'error');
|
| 1646 |
+
}
|
| 1647 |
+
}
|
| 1648 |
+
|
| 1649 |
+
function downloadDiagnosticLog() {
|
| 1650 |
+
const output = document.getElementById('diagnostic-output').textContent;
|
| 1651 |
+
if (!output.trim()) {
|
| 1652 |
+
showToast('β No diagnostic output to download', 'warning');
|
| 1653 |
+
return;
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
const blob = new Blob([output], { type: 'text/plain' });
|
| 1657 |
+
const url = URL.createObjectURL(blob);
|
| 1658 |
+
const a = document.createElement('a');
|
| 1659 |
+
a.href = url;
|
| 1660 |
+
a.download = `diagnostic-log-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`;
|
| 1661 |
+
document.body.appendChild(a);
|
| 1662 |
+
a.click();
|
| 1663 |
+
document.body.removeChild(a);
|
| 1664 |
+
URL.revokeObjectURL(url);
|
| 1665 |
+
|
| 1666 |
+
showToast('β
Log downloaded', 'success');
|
| 1667 |
+
}
|
| 1668 |
+
|
| 1669 |
+
// Initialize diagnostics on page load
|
| 1670 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 1671 |
+
// Update status cards on page load
|
| 1672 |
+
refreshDiagnosticStatus();
|
| 1673 |
+
});
|
| 1674 |
+
|
| 1675 |
+
// Export diagnostic functions to window
|
| 1676 |
+
window.runDiagnostic = runDiagnostic;
|
| 1677 |
+
window.refreshDiagnosticStatus = refreshDiagnosticStatus;
|
| 1678 |
+
window.downloadDiagnosticLog = downloadDiagnosticLog;
|
| 1679 |
+
|
| 1680 |
console.log('β
App.js loaded successfully');
|