Really-amin commited on
Commit
407dc70
Β·
verified Β·
1 Parent(s): 8df9a6d

Upload 54 files

Browse files
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
- Get comprehensive system status overview
47
-
48
- Returns:
49
- System overview with provider counts, health metrics, and last update
50
- """
51
- try:
52
- # Get latest system metrics from database
53
- latest_metrics = db_manager.get_latest_system_metrics()
54
-
55
- if latest_metrics:
56
- return {
57
- "total_apis": latest_metrics.total_providers,
58
- "online": latest_metrics.online_count,
59
- "degraded": latest_metrics.degraded_count,
60
- "offline": latest_metrics.offline_count,
61
- "avg_response_time_ms": round(latest_metrics.avg_response_time_ms, 2),
62
- "last_update": latest_metrics.timestamp.isoformat(),
63
- "system_health": latest_metrics.system_health
64
- }
65
-
66
- # Fallback: Calculate from providers if no metrics available
67
- providers = db_manager.get_all_providers()
68
-
69
- # Get recent connection attempts for each provider
70
- status_counts = {"online": 0, "degraded": 0, "offline": 0}
71
- response_times = []
72
-
73
- for provider in providers:
74
- attempts = db_manager.get_connection_attempts(
75
- provider_id=provider.id,
76
- hours=1,
77
- limit=10
78
- )
79
-
80
- if attempts:
81
- recent = attempts[0]
82
- if recent.status == "success" and recent.response_time_ms and recent.response_time_ms < 2000:
83
- status_counts["online"] += 1
84
- response_times.append(recent.response_time_ms)
85
- elif recent.status == "success":
86
- status_counts["degraded"] += 1
87
- if recent.response_time_ms:
88
- response_times.append(recent.response_time_ms)
89
- else:
90
- status_counts["offline"] += 1
91
- else:
92
- status_counts["offline"] += 1
93
-
94
- avg_response_time = sum(response_times) / len(response_times) if response_times else 0
95
-
96
- # Determine system health
97
- total = len(providers)
98
- online_pct = (status_counts["online"] / total * 100) if total > 0 else 0
99
-
100
- if online_pct >= 90:
101
- system_health = "healthy"
102
- elif online_pct >= 70:
103
- system_health = "degraded"
104
- else:
105
- system_health = "unhealthy"
106
-
107
- return {
108
- "total_apis": total,
109
- "online": status_counts["online"],
110
- "degraded": status_counts["degraded"],
111
- "offline": status_counts["offline"],
112
- "avg_response_time_ms": round(avg_response_time, 2),
113
- "last_update": datetime.utcnow().isoformat(),
114
- "system_health": system_health
115
- }
116
-
117
- except Exception as e:
118
- logger.error(f"Error getting system status: {e}", exc_info=True)
119
- raise HTTPException(status_code=500, detail=f"Failed to get system status: {str(e)}")
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
- category: Optional[str] = Query(None, description="Filter by category"),
211
- status: Optional[str] = Query(None, description="Filter by status (online/degraded/offline)"),
212
- search: Optional[str] = Query(None, description="Search by provider name")
213
- ):
214
- """
215
- Get list of providers with optional filtering
216
-
217
- Args:
218
- category: Filter by provider category
219
- status: Filter by provider status
220
- search: Search by provider name
221
-
222
- Returns:
223
- List of providers with detailed information
224
- """
225
- try:
226
- # Get providers from database
227
- providers = db_manager.get_all_providers(category=category)
228
-
229
- result = []
230
-
231
- for provider in providers:
232
- # Apply search filter
233
- if search and search.lower() not in provider.name.lower():
234
- continue
235
-
236
- # Get recent connection attempts
237
- attempts = db_manager.get_connection_attempts(
238
- provider_id=provider.id,
239
- hours=1,
240
- limit=10
241
- )
242
-
243
- # Determine provider status
244
- provider_status = "offline"
245
- response_time_ms = 0
246
- last_fetch = None
247
-
248
- if attempts:
249
- recent = attempts[0]
250
- last_fetch = recent.timestamp
251
-
252
- if recent.status == "success":
253
- if recent.response_time_ms and recent.response_time_ms < 2000:
254
- provider_status = "online"
255
- else:
256
- provider_status = "degraded"
257
- response_time_ms = recent.response_time_ms or 0
258
- elif recent.status == "rate_limited":
259
- provider_status = "degraded"
260
- else:
261
- provider_status = "offline"
262
-
263
- # Apply status filter
264
- if status and provider_status != status:
265
- continue
266
-
267
- # Get rate limit info
268
- rate_limit_status = rate_limiter.get_status(provider.name)
269
- rate_limit = None
270
- if rate_limit_status:
271
- rate_limit = f"{rate_limit_status['current_usage']}/{rate_limit_status['limit_value']} {rate_limit_status['limit_type']}"
272
- elif provider.rate_limit_type and provider.rate_limit_value:
273
- rate_limit = f"0/{provider.rate_limit_value} {provider.rate_limit_type}"
274
-
275
- # Get schedule config
276
- schedule_config = db_manager.get_schedule_config(provider.id)
277
-
278
- result.append({
279
- "id": provider.id,
280
- "name": provider.name,
281
- "category": provider.category,
282
- "status": provider_status,
283
- "response_time_ms": response_time_ms,
284
- "rate_limit": rate_limit,
285
- "last_fetch": last_fetch.isoformat() if last_fetch else None,
286
- "has_key": provider.requires_key,
287
- "endpoints": provider.endpoint_url
288
- })
289
-
290
- return result
291
-
292
- except Exception as e:
293
- logger.error(f"Error getting providers: {e}", exc_info=True)
294
- raise HTTPException(status_code=500, detail=f"Failed to get providers: {str(e)}")
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
- @media (max-width: 1400px) {
 
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(auto-fit, minmax(280px, 1fr));
975
  gap: 25px;
976
  }
977
 
@@ -1144,7 +1152,7 @@ table tr:hover {
1144
  }
1145
 
1146
  .stats-grid {
1147
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
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(auto-fit, minmax(380px, 1fr));
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');