Really-amin commited on
Commit
c2612d8
ยท
verified ยท
1 Parent(s): 7ee650f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1949 -230
app.py CHANGED
@@ -1,102 +1,34 @@
1
  #!/usr/bin/env python3
2
  """
3
  Crypto API Monitor ULTIMATE - Real API Integration
 
4
  Fixed for Hugging Face Spaces deployment
5
  """
6
 
7
- import os
8
- import sys
9
- import json
10
- import random
 
 
11
  import asyncio
 
 
 
12
  import logging
13
- from pathlib import Path
14
- from typing import List, Dict, Optional, Literal
15
  from datetime import datetime, timedelta
 
16
  from collections import defaultdict
17
- from threading import Lock
18
  from urllib.parse import urljoin, unquote
 
 
19
 
20
- import aiohttp
21
- import uvicorn
22
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
23
- from fastapi.responses import HTMLResponse, FileResponse, Response
24
- from fastapi.staticfiles import StaticFiles
25
- from fastapi.middleware.cors import CORSMiddleware
26
  from starlette.middleware.trustedhost import TrustedHostMiddleware
27
- from pydantic import BaseModel
28
-
29
- # Setup logging
30
- logging.basicConfig(
31
- level=logging.INFO,
32
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
33
- )
34
- logger = logging.getLogger("crypto_monitor")
35
 
36
- print("=" * 70)
37
- print("๐Ÿš€ Crypto Monitor ULTIMATE - Initializing...")
38
- print("=" * 70)
39
-
40
- # Try to import optional modules with fallbacks
41
- try:
42
- from database import Database
43
- HAS_DATABASE = True
44
- logger.info("โœ“ Database module loaded")
45
- except ImportError as e:
46
- logger.warning(f"โš  Database module not found: {e}")
47
- HAS_DATABASE = False
48
- # Create mock Database class
49
- class Database:
50
- def __init__(self, *args, **kwargs):
51
- pass
52
- def get_uptime_percentage(self, *args): return 0.0
53
- def get_avg_response_time(self, *args): return 0.0
54
- def log_provider_status(self, *args, **kwargs): pass
55
- def get_recent_status(self, *args, **kwargs): return []
56
- def get_unacknowledged_alerts(self): return []
57
- def get_pools(self): return []
58
- def get_pool(self, pool_id): return None
59
- def create_pool(self, *args, **kwargs): return 1
60
- def delete_pool(self, *args): pass
61
- def add_pool_member(self, *args, **kwargs): pass
62
- def remove_pool_member(self, *args): pass
63
- def update_member_stats(self, *args, **kwargs): pass
64
- def increment_member_use(self, *args): pass
65
- def log_pool_rotation(self, *args, **kwargs): pass
66
- def get_pool_rotation_history(self, *args, **kwargs): return []
67
-
68
- try:
69
- from config import config as global_config
70
- HAS_CONFIG = True
71
- logger.info("โœ“ Config module loaded")
72
- except ImportError as e:
73
- logger.warning(f"โš  Config module not found: {e}")
74
- HAS_CONFIG = False
75
- # Create mock config
76
- class MockConfig:
77
- def get_provider(self, name): return None
78
- def get_all_providers(self): return []
79
- global_config = MockConfig()
80
-
81
- try:
82
- from backend.feature_flags import feature_flags, is_feature_enabled
83
- HAS_FEATURE_FLAGS = True
84
- logger.info("โœ“ Feature flags module loaded")
85
- except ImportError as e:
86
- logger.warning(f"โš  Feature flags module not found: {e}")
87
- HAS_FEATURE_FLAGS = False
88
- # Create mock feature flags
89
- class MockFeatureFlags:
90
- def get_flag(self, name): return False
91
- def set_flag(self, name, value): return True
92
- def get_all_flags(self): return {}
93
- def update_flags(self, flags): return True
94
- def reset_to_defaults(self): return True
95
- def get_feature_info(self): return {}
96
- feature_flags = MockFeatureFlags()
97
- def is_feature_enabled(name): return False
98
-
99
- # Pydantic Models
100
  class SentimentRequest(BaseModel):
101
  texts: List[str]
102
 
@@ -122,6 +54,7 @@ class ProviderCreateRequest(BaseModel):
122
  health_check_endpoint: Optional[str] = None
123
  notes: Optional[str] = None
124
 
 
125
  class HFRegistryItemCreate(BaseModel):
126
  id: str
127
  kind: Literal["model", "dataset"]
@@ -136,21 +69,26 @@ class FeatureFlagUpdate(BaseModel):
136
  class FeatureFlagsUpdate(BaseModel):
137
  flags: Dict[str, bool]
138
 
139
- # Initialize FastAPI app
140
- app = FastAPI(
141
- title="Crypto Monitor Ultimate",
142
- version="3.0.0",
143
- description="Real-time cryptocurrency monitoring with 100+ free APIs"
144
  )
 
 
 
 
 
 
 
 
145
 
146
- logger.info("โœ“ FastAPI app initialized")
147
 
148
- # CORS Configuration
149
  def _split_env_list(value: Optional[str]) -> List[str]:
150
  if not value:
151
  return []
152
  return [item.strip() for item in value.split(",") if item.strip()]
153
 
 
154
  allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "")
155
  allowed_origin_regex_env = os.getenv("ALLOWED_ORIGIN_REGEX")
156
  allowed_origins = _split_env_list(allowed_origins_env)
@@ -175,9 +113,7 @@ if not trusted_hosts:
175
  trusted_hosts = ["*"]
176
  app.add_middleware(TrustedHostMiddleware, allowed_hosts=trusted_hosts)
177
 
178
- logger.info("โœ“ Middleware configured")
179
 
180
- # Custom Registry
181
  CUSTOM_REGISTRY_PATH = Path("data/custom_registry.json")
182
  _registry_lock = Lock()
183
  _custom_registry: Dict[str, List[Dict]] = {
@@ -186,9 +122,14 @@ _custom_registry: Dict[str, List[Dict]] = {
186
  "hf_datasets": []
187
  }
188
 
 
189
  def _load_custom_registry() -> Dict[str, List[Dict]]:
190
  if not CUSTOM_REGISTRY_PATH.exists():
191
- return {"providers": [], "hf_models": [], "hf_datasets": []}
 
 
 
 
192
  try:
193
  with CUSTOM_REGISTRY_PATH.open("r", encoding="utf-8") as f:
194
  data = json.load(f)
@@ -197,20 +138,26 @@ def _load_custom_registry() -> Dict[str, List[Dict]]:
197
  "hf_models": data.get("hf_models", []),
198
  "hf_datasets": data.get("hf_datasets", []),
199
  }
200
- except Exception as e:
201
- logger.warning(f"Failed to load custom registry: {e}")
202
- return {"providers": [], "hf_models": [], "hf_datasets": []}
 
 
 
 
203
 
204
  def _save_custom_registry() -> None:
205
  CUSTOM_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
206
  with CUSTOM_REGISTRY_PATH.open("w", encoding="utf-8") as f:
207
  json.dump(_custom_registry, f, ensure_ascii=False, indent=2)
208
 
 
209
  def _refresh_custom_registry() -> None:
210
  global _custom_registry
211
  with _registry_lock:
212
  _custom_registry = _load_custom_registry()
213
 
 
214
  _refresh_custom_registry()
215
 
216
  # WebSocket Manager
@@ -223,8 +170,7 @@ class ConnectionManager:
223
  self.active_connections.append(websocket)
224
 
225
  def disconnect(self, websocket: WebSocket):
226
- if websocket in self.active_connections:
227
- self.active_connections.remove(websocket)
228
 
229
  async def broadcast(self, message: dict):
230
  for connection in self.active_connections:
@@ -235,11 +181,9 @@ class ConnectionManager:
235
 
236
  manager = ConnectionManager()
237
 
238
- # Initialize Database
239
  db = Database("data/crypto_monitor.db")
240
- logger.info("โœ“ Database initialized")
241
 
242
- # API Provider Configuration
243
  API_PROVIDERS = {
244
  "market_data": [
245
  {
@@ -251,31 +195,118 @@ API_PROVIDERS = {
251
  "global": "/global",
252
  "trending": "/search/trending"
253
  },
 
254
  "rate_limit": "50/min",
255
  "status": "active"
256
  },
257
  {
258
  "name": "CoinCap",
259
  "base_url": "https://api.coincap.io/v2",
260
- "endpoints": {"assets": "/assets", "rates": "/rates"},
 
 
 
 
261
  "rate_limit": "200/min",
262
  "status": "active"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
  ],
265
  "exchanges": [
266
  {
267
  "name": "Binance",
268
  "base_url": "https://api.binance.com/api/v3",
269
- "endpoints": {"ticker": "/ticker/24hr", "price": "/ticker/price"},
 
 
 
 
270
  "rate_limit": "1200/min",
271
  "status": "active"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
  ],
274
  "sentiment": [
275
  {
276
  "name": "Alternative.me Fear & Greed",
277
  "base_url": "https://api.alternative.me",
278
- "endpoints": {"fng": "/fng/?limit=1&format=json"},
 
 
 
279
  "rate_limit": "unlimited",
280
  "status": "active"
281
  }
@@ -284,72 +315,348 @@ API_PROVIDERS = {
284
  {
285
  "name": "DeFi Llama",
286
  "base_url": "https://api.llama.fi",
287
- "endpoints": {"protocols": "/protocols", "tvl": "/tvl"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  "rate_limit": "unlimited",
289
  "status": "active"
 
 
 
 
 
 
 
 
 
 
290
  }
291
  ]
292
  }
293
 
294
- # Cache
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  cache = {
296
  "market_data": {"data": None, "timestamp": None, "ttl": 60},
 
297
  "sentiment": {"data": None, "timestamp": None, "ttl": 3600},
298
  "defi": {"data": None, "timestamp": None, "ttl": 300}
299
  }
300
 
301
- provider_health_cache: Dict[str, Dict] = {}
302
  provider_proxy_cache: Dict[str, Dict] = {}
303
 
304
- CORS_PROXIES = ['https://api.allorigins.win/get?url=']
 
 
 
 
305
 
306
- # Helper Functions
307
- def provider_slug(name: str) -> str:
308
- return name.lower().replace(" ", "_")
309
 
310
- def is_cache_valid(cache_entry):
311
- if cache_entry["data"] is None or cache_entry["timestamp"] is None:
312
  return False
313
- elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
314
- return elapsed < cache_entry["ttl"]
315
 
316
- async def fetch_with_retry(session, url, retries=3):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  for attempt in range(retries):
318
  try:
319
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
320
  if response.status == 200:
 
 
321
  return await response.json()
322
  elif response.status == 429:
323
  await asyncio.sleep(2 ** attempt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  except Exception as e:
325
  if attempt == retries - 1:
326
  logger.debug(f"Error fetching {url}: {e}")
327
  return None
328
  await asyncio.sleep(1)
 
329
  return None
330
 
331
- def assemble_providers() -> List[Dict]:
332
- providers: List[Dict] = []
333
- for category, provider_list in API_PROVIDERS.items():
334
- for provider in provider_list:
335
- providers.append({
336
- "name": provider["name"],
337
- "category": category,
338
- "base_url": provider["base_url"],
339
- "endpoints": provider.get("endpoints", {}),
340
- "rate_limit": provider.get("rate_limit", ""),
341
- "status": provider.get("status", "unknown"),
342
- "requires_key": False,
343
- "timeout_ms": 10000
344
- })
345
- return providers
346
 
347
  async def get_market_data():
348
  if is_cache_valid(cache["market_data"]):
349
  return cache["market_data"]["data"]
350
 
351
  async with aiohttp.ClientSession() as session:
352
- url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=20&page=1"
353
  data = await fetch_with_retry(session, url)
354
 
355
  if data:
@@ -369,6 +676,70 @@ async def get_market_data():
369
  cache["market_data"]["data"] = formatted_data
370
  cache["market_data"]["timestamp"] = datetime.now()
371
  return formatted_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  return []
374
 
@@ -393,98 +764,237 @@ async def get_sentiment():
393
 
394
  return {"value": 50, "classification": "Neutral", "timestamp": ""}
395
 
396
- async def get_provider_stats(force_refresh: bool = False):
397
- providers = assemble_providers()
398
- return [{
399
- "name": p["name"],
400
- "category": p["category"],
401
- "base_url": p["base_url"],
402
- "status": "online",
403
- "uptime": 99.5,
404
- "response_time_ms": 150,
405
- "rate_limit": p.get("rate_limit", ""),
406
- "endpoints": len(p.get("endpoints", {})),
407
- "last_check": datetime.now().isoformat()
408
- } for p in providers]
409
-
410
- # API Endpoints
411
- @app.get("/")
412
- async def root():
413
- return HTMLResponse(content="""
414
- <!DOCTYPE html>
415
- <html>
416
- <head>
417
- <title>Crypto Monitor Ultimate</title>
418
- <meta charset="utf-8">
419
- <meta name="viewport" content="width=device-width, initial-scale=1">
420
- <style>
421
- body {
422
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
423
- max-width: 800px; margin: 50px auto; padding: 20px;
424
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
425
- color: white;
426
- }
427
- .container { background: rgba(255,255,255,0.1); padding: 40px; border-radius: 20px; }
428
- h1 { font-size: 3em; margin: 0 0 20px 0; }
429
- .status { background: rgba(0,255,0,0.2); padding: 15px; border-radius: 10px; margin: 20px 0; }
430
- a { color: #fff; text-decoration: none; background: rgba(255,255,255,0.2);
431
- padding: 10px 20px; border-radius: 5px; display: inline-block; margin: 5px; }
432
- a:hover { background: rgba(255,255,255,0.3); }
433
- </style>
434
- </head>
435
- <body>
436
- <div class="container">
437
- <h1>๐Ÿš€ Crypto Monitor Ultimate</h1>
438
- <div class="status">
439
- <strong>โœ… Status:</strong> Online and Running<br>
440
- <strong>๐ŸŒ Port:</strong> """ + str(os.getenv("PORT", 7860)) + """<br>
441
- <strong>๐Ÿ“Š Version:</strong> 3.0.0
442
- </div>
443
- <h2>Available Endpoints:</h2>
444
- <a href="/docs">๐Ÿ“š API Documentation</a>
445
- <a href="/health">๐Ÿฅ Health Check</a>
446
- <a href="/api/market">๐Ÿ’ฐ Market Data</a>
447
- <a href="/api/sentiment">๐Ÿ˜จ Fear & Greed Index</a>
448
- <a href="/api/providers">๐Ÿ”Œ API Providers</a>
449
- </div>
450
- </body>
451
- </html>
452
- """)
453
-
454
- @app.get("/health")
455
- async def health():
456
- providers = await get_provider_stats()
457
- return {
458
- "status": "healthy",
459
- "timestamp": datetime.now().isoformat(),
460
- "providers": {
461
- "total": len(providers),
462
- "operational": len([p for p in providers if p["status"] == "online"])
463
- },
464
- "port": int(os.getenv("PORT", 7860)),
465
- "version": "3.0.0"
466
  }
467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  @app.get("/api/info")
469
  async def api_info():
 
470
  return {
471
  "name": "Crypto Monitor Ultimate",
472
  "version": "3.0.0",
473
  "description": "Real-time crypto monitoring with 100+ free APIs",
474
- "total_providers": sum(len(p) for p in API_PROVIDERS.values()),
475
  "categories": list(API_PROVIDERS.keys()),
476
- "port": int(os.getenv("PORT", 7860)),
477
- "environment": "Hugging Face Spaces" if os.getenv("SPACE_ID") else "Local"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
 
 
 
 
 
 
480
  @app.get("/api/market")
481
  async def market():
482
  data = await get_market_data()
 
 
483
  return {
484
  "cryptocurrencies": data,
 
 
 
 
 
 
 
 
 
 
485
  "timestamp": datetime.now().isoformat(),
486
- "source": "CoinGecko",
487
- "count": len(data)
488
  }
489
 
490
  @app.get("/api/sentiment")
@@ -496,32 +1006,201 @@ async def sentiment():
496
  "source": "Alternative.me"
497
  }
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  @app.get("/api/providers")
500
  async def providers():
501
  data = await get_provider_stats()
502
  return data
503
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  @app.get("/api/status")
505
  async def status():
506
  providers = await get_provider_stats()
 
 
 
 
 
 
 
 
 
 
 
507
  return {
508
  "total_providers": len(providers),
509
- "online": len([p for p in providers if p["status"] == "online"]),
510
- "system_health": "healthy",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  "timestamp": datetime.now().isoformat()
512
  }
513
 
514
  @app.websocket("/ws")
 
 
 
 
515
  async def websocket_endpoint(websocket: WebSocket):
516
  await manager.connect(websocket)
517
  try:
518
  await websocket.send_json({
519
  "type": "welcome",
520
- "message": "Connected to Crypto Monitor"
 
521
  })
522
 
523
  while True:
524
- await asyncio.sleep(30)
 
525
  market_data = await get_market_data()
526
  if market_data:
527
  await websocket.send_json({
@@ -530,35 +1209,1075 @@ async def websocket_endpoint(websocket: WebSocket):
530
  "timestamp": datetime.now().isoformat()
531
  })
532
 
 
 
 
 
 
 
 
 
533
  except WebSocketDisconnect:
534
  manager.disconnect(websocket)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
  manager.disconnect(websocket)
537
 
538
- # Startup message
539
- @app.on_event("startup")
540
- async def startup_event():
541
- port = int(os.getenv("PORT", 7860))
542
- logger.info("=" * 70)
543
- logger.info("๐Ÿš€ Crypto Monitor ULTIMATE - Started Successfully!")
544
- logger.info(f"๐ŸŒ Server running on port: {port}")
545
- logger.info(f"๐Ÿ“ก API Docs: http://localhost:{port}/docs")
546
- logger.info(f"๐Ÿฅ Health: http://localhost:{port}/health")
547
- logger.info("=" * 70)
548
-
549
- # Main entry point
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  if __name__ == "__main__":
 
 
 
551
  port = int(os.getenv("PORT", 7860))
552
  host = os.getenv("HOST", "0.0.0.0")
553
 
554
- print("\n" + "=" * 70)
555
  print("๐Ÿš€ Crypto Monitor ULTIMATE")
556
- print("=" * 70)
557
- print(f"๐Ÿ“Š Real APIs: CoinGecko, Binance, Alternative.me")
558
  print(f"๐ŸŒ Server: http://{host}:{port}")
559
  print(f"๐Ÿ“ก API Docs: http://{host}:{port}/docs")
560
- print(f"๐ŸŽฏ Environment: {'Hugging Face Spaces' if port == 7860 else 'Local'}")
561
- print("=" * 70 + "\n")
562
 
563
  uvicorn.run(
564
  app,
 
1
  #!/usr/bin/env python3
2
  """
3
  Crypto API Monitor ULTIMATE - Real API Integration
4
+ Complete professional monitoring system with 100+ real free crypto APIs
5
  Fixed for Hugging Face Spaces deployment
6
  """
7
 
8
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
9
+ from fastapi.responses import HTMLResponse, FileResponse, Response
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel
13
+ from typing import List, Dict, Optional, Literal
14
  import asyncio
15
+ import aiohttp
16
+ import random
17
+ import json
18
  import logging
 
 
19
  from datetime import datetime, timedelta
20
+ import uvicorn
21
  from collections import defaultdict
22
+ import os
23
  from urllib.parse import urljoin, unquote
24
+ from pathlib import Path
25
+ from threading import Lock
26
 
27
+ from database import Database
28
+ from config import config as global_config
 
 
 
 
29
  from starlette.middleware.trustedhost import TrustedHostMiddleware
30
+ from backend.feature_flags import feature_flags, is_feature_enabled
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  class SentimentRequest(BaseModel):
33
  texts: List[str]
34
 
 
54
  health_check_endpoint: Optional[str] = None
55
  notes: Optional[str] = None
56
 
57
+
58
  class HFRegistryItemCreate(BaseModel):
59
  id: str
60
  kind: Literal["model", "dataset"]
 
69
  class FeatureFlagsUpdate(BaseModel):
70
  flags: Dict[str, bool]
71
 
72
+ logging.basicConfig(
73
+ level=logging.INFO,
74
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 
 
75
  )
76
+ logger = logging.getLogger("crypto_monitor")
77
+
78
+ # Initialize FastAPI app
79
+ app = FastAPI(title="Crypto Monitor Ultimate", version="3.0.0")
80
+
81
+ print("=" * 70)
82
+ print("๐Ÿš€ Crypto Monitor ULTIMATE - Starting...")
83
+ print("=" * 70)
84
 
 
85
 
 
86
  def _split_env_list(value: Optional[str]) -> List[str]:
87
  if not value:
88
  return []
89
  return [item.strip() for item in value.split(",") if item.strip()]
90
 
91
+
92
  allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "")
93
  allowed_origin_regex_env = os.getenv("ALLOWED_ORIGIN_REGEX")
94
  allowed_origins = _split_env_list(allowed_origins_env)
 
113
  trusted_hosts = ["*"]
114
  app.add_middleware(TrustedHostMiddleware, allowed_hosts=trusted_hosts)
115
 
 
116
 
 
117
  CUSTOM_REGISTRY_PATH = Path("data/custom_registry.json")
118
  _registry_lock = Lock()
119
  _custom_registry: Dict[str, List[Dict]] = {
 
122
  "hf_datasets": []
123
  }
124
 
125
+
126
  def _load_custom_registry() -> Dict[str, List[Dict]]:
127
  if not CUSTOM_REGISTRY_PATH.exists():
128
+ return {
129
+ "providers": [],
130
+ "hf_models": [],
131
+ "hf_datasets": []
132
+ }
133
  try:
134
  with CUSTOM_REGISTRY_PATH.open("r", encoding="utf-8") as f:
135
  data = json.load(f)
 
138
  "hf_models": data.get("hf_models", []),
139
  "hf_datasets": data.get("hf_datasets", []),
140
  }
141
+ except Exception:
142
+ return {
143
+ "providers": [],
144
+ "hf_models": [],
145
+ "hf_datasets": []
146
+ }
147
+
148
 
149
  def _save_custom_registry() -> None:
150
  CUSTOM_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
151
  with CUSTOM_REGISTRY_PATH.open("w", encoding="utf-8") as f:
152
  json.dump(_custom_registry, f, ensure_ascii=False, indent=2)
153
 
154
+
155
  def _refresh_custom_registry() -> None:
156
  global _custom_registry
157
  with _registry_lock:
158
  _custom_registry = _load_custom_registry()
159
 
160
+
161
  _refresh_custom_registry()
162
 
163
  # WebSocket Manager
 
170
  self.active_connections.append(websocket)
171
 
172
  def disconnect(self, websocket: WebSocket):
173
+ self.active_connections.remove(websocket)
 
174
 
175
  async def broadcast(self, message: dict):
176
  for connection in self.active_connections:
 
181
 
182
  manager = ConnectionManager()
183
 
 
184
  db = Database("data/crypto_monitor.db")
 
185
 
186
+ # API Provider Configuration - Real Free APIs
187
  API_PROVIDERS = {
188
  "market_data": [
189
  {
 
195
  "global": "/global",
196
  "trending": "/search/trending"
197
  },
198
+ "auth": None,
199
  "rate_limit": "50/min",
200
  "status": "active"
201
  },
202
  {
203
  "name": "CoinCap",
204
  "base_url": "https://api.coincap.io/v2",
205
+ "endpoints": {
206
+ "assets": "/assets",
207
+ "rates": "/rates"
208
+ },
209
+ "auth": None,
210
  "rate_limit": "200/min",
211
  "status": "active"
212
+ },
213
+ {
214
+ "name": "CoinStats",
215
+ "base_url": "https://api.coinstats.app",
216
+ "endpoints": {
217
+ "coins": "/public/v1/coins",
218
+ "charts": "/public/v1/charts"
219
+ },
220
+ "auth": None,
221
+ "rate_limit": "unlimited",
222
+ "status": "active"
223
+ },
224
+ {
225
+ "name": "Cryptorank",
226
+ "base_url": "https://api.cryptorank.io/v1",
227
+ "endpoints": {
228
+ "currencies": "/currencies"
229
+ },
230
+ "auth": None,
231
+ "rate_limit": "100/min",
232
+ "status": "active"
233
  }
234
  ],
235
  "exchanges": [
236
  {
237
  "name": "Binance",
238
  "base_url": "https://api.binance.com/api/v3",
239
+ "endpoints": {
240
+ "ticker": "/ticker/24hr",
241
+ "price": "/ticker/price"
242
+ },
243
+ "auth": None,
244
  "rate_limit": "1200/min",
245
  "status": "active"
246
+ },
247
+ {
248
+ "name": "Coinbase",
249
+ "base_url": "https://api.coinbase.com/v2",
250
+ "endpoints": {
251
+ "prices": "/prices",
252
+ "exchange_rates": "/exchange-rates"
253
+ },
254
+ "auth": None,
255
+ "rate_limit": "10000/hour",
256
+ "status": "active"
257
+ },
258
+ {
259
+ "name": "Kraken",
260
+ "base_url": "https://api.kraken.com/0/public",
261
+ "endpoints": {
262
+ "ticker": "/Ticker",
263
+ "trades": "/Trades"
264
+ },
265
+ "auth": None,
266
+ "rate_limit": "1/sec",
267
+ "status": "active"
268
+ }
269
+ ],
270
+ "news": [
271
+ {
272
+ "name": "CoinStats News",
273
+ "base_url": "https://api.coinstats.app",
274
+ "endpoints": {
275
+ "feed": "/public/v1/news"
276
+ },
277
+ "auth": None,
278
+ "rate_limit": "unlimited",
279
+ "status": "active"
280
+ },
281
+ {
282
+ "name": "CoinDesk RSS",
283
+ "base_url": "https://www.coindesk.com",
284
+ "endpoints": {
285
+ "rss": "/arc/outboundfeeds/rss/?outputType=xml"
286
+ },
287
+ "auth": None,
288
+ "rate_limit": "unlimited",
289
+ "status": "active"
290
+ },
291
+ {
292
+ "name": "Cointelegraph RSS",
293
+ "base_url": "https://cointelegraph.com",
294
+ "endpoints": {
295
+ "rss": "/rss"
296
+ },
297
+ "auth": None,
298
+ "rate_limit": "unlimited",
299
+ "status": "active"
300
  }
301
  ],
302
  "sentiment": [
303
  {
304
  "name": "Alternative.me Fear & Greed",
305
  "base_url": "https://api.alternative.me",
306
+ "endpoints": {
307
+ "fng": "/fng/?limit=1&format=json"
308
+ },
309
+ "auth": None,
310
  "rate_limit": "unlimited",
311
  "status": "active"
312
  }
 
315
  {
316
  "name": "DeFi Llama",
317
  "base_url": "https://api.llama.fi",
318
+ "endpoints": {
319
+ "protocols": "/protocols",
320
+ "tvl": "/tvl"
321
+ },
322
+ "auth": None,
323
+ "rate_limit": "unlimited",
324
+ "status": "active"
325
+ },
326
+ {
327
+ "name": "1inch",
328
+ "base_url": "https://api.1inch.io/v5.0/1",
329
+ "endpoints": {
330
+ "quote": "/quote"
331
+ },
332
+ "auth": None,
333
+ "rate_limit": "unlimited",
334
+ "status": "active"
335
+ }
336
+ ],
337
+ "blockchain": [
338
+ {
339
+ "name": "Blockscout Ethereum",
340
+ "base_url": "https://eth.blockscout.com/api",
341
+ "endpoints": {
342
+ "balance": "/v2/addresses"
343
+ },
344
+ "auth": None,
345
  "rate_limit": "unlimited",
346
  "status": "active"
347
+ },
348
+ {
349
+ "name": "Ethplorer",
350
+ "base_url": "https://api.ethplorer.io",
351
+ "endpoints": {
352
+ "address": "/getAddressInfo"
353
+ },
354
+ "auth": {"type": "query", "key": "freekey"},
355
+ "rate_limit": "limited",
356
+ "status": "active"
357
  }
358
  ]
359
  }
360
 
361
+ DEFI_FALLBACK = [
362
+ {
363
+ "name": "Sample Protocol",
364
+ "tvl": 0.0,
365
+ "change_24h": 0.0,
366
+ "chain": "N/A",
367
+ }
368
+ ]
369
+
370
+ # Health check configuration
371
+ HEALTH_TESTS = {
372
+ "CoinGecko": {"path": "/ping"},
373
+ "CoinCap": {"path": "/assets/bitcoin", "params": {"limit": 1}},
374
+ "CoinStats": {"path": "/public/v1/coins", "params": {"skip": 0, "limit": 1}},
375
+ "CoinStats News": {"path": "/public/v1/news", "params": {"skip": 0, "limit": 1}},
376
+ "Cryptorank": {"path": "/currencies"},
377
+ "Binance": {"path": "/ping"},
378
+ "Coinbase": {"path": "/exchange-rates"},
379
+ "Kraken": {"path": "/SystemStatus"},
380
+ "Alternative.me Fear & Greed": {"path": "/fng/", "params": {"limit": 1, "format": "json"}},
381
+ "DeFi Llama": {"path": "/protocols"},
382
+ "1inch": {"path": "/tokens"},
383
+ "Blockscout Ethereum": {"path": "/v2/stats"},
384
+ "Ethplorer": {"path": "/getTop", "params": {"apikey": "freekey"}},
385
+ "CoinDesk RSS": {"path": "/arc/outboundfeeds/rss/?outputType=xml"},
386
+ "Cointelegraph RSS": {"path": "/rss"}
387
+ }
388
+
389
+ KEY_HEADER_MAP = {
390
+ "CoinMarketCap": ("X-CMC_PRO_API_KEY", "plain"),
391
+ "CryptoCompare": ("Authorization", "apikey")
392
+ }
393
+
394
+ KEY_QUERY_MAP = {
395
+ "Etherscan": "apikey",
396
+ "BscScan": "apikey",
397
+ "TronScan": "apikey"
398
+ }
399
+
400
+ HEALTH_CACHE_TTL = 120
401
+ provider_health_cache: Dict[str, Dict] = {}
402
+
403
+
404
+ def provider_slug(name: str) -> str:
405
+ return name.lower().replace(" ", "_")
406
+
407
+
408
+ def _get_custom_providers() -> List[Dict]:
409
+ with _registry_lock:
410
+ return [dict(provider) for provider in _custom_registry.get("providers", [])]
411
+
412
+
413
+ def _add_custom_provider(payload: Dict) -> Dict:
414
+ slug = provider_slug(payload["name"])
415
+ with _registry_lock:
416
+ existing = _custom_registry.setdefault("providers", [])
417
+ if any(provider_slug(item.get("name", "")) == slug for item in existing):
418
+ raise ValueError("Provider already exists")
419
+ existing.append(payload)
420
+ _save_custom_registry()
421
+ return payload
422
+
423
+
424
+ def _remove_custom_provider(slug: str) -> bool:
425
+ removed = False
426
+ with _registry_lock:
427
+ providers = _custom_registry.setdefault("providers", [])
428
+ new_list = []
429
+ for item in providers:
430
+ if provider_slug(item.get("name", "")) == slug:
431
+ removed = True
432
+ continue
433
+ new_list.append(item)
434
+ if removed:
435
+ _custom_registry["providers"] = new_list
436
+ _save_custom_registry()
437
+ return removed
438
+
439
+
440
+ def _get_custom_hf(kind: Literal["models", "datasets"]) -> List[Dict]:
441
+ key = "hf_models" if kind == "models" else "hf_datasets"
442
+ with _registry_lock:
443
+ return [dict(item) for item in _custom_registry.get(key, [])]
444
+
445
+
446
+ def _add_custom_hf_item(kind: Literal["models", "datasets"], payload: Dict) -> Dict:
447
+ key = "hf_models" if kind == "models" else "hf_datasets"
448
+ identifier = payload.get("id") or payload.get("name")
449
+ if not identifier:
450
+ raise ValueError("id is required")
451
+ with _registry_lock:
452
+ collection = _custom_registry.setdefault(key, [])
453
+ if any((item.get("id") or item.get("name")) == identifier for item in collection):
454
+ raise ValueError("Item already exists")
455
+ collection.append(payload)
456
+ _save_custom_registry()
457
+ return payload
458
+
459
+
460
+ def _remove_custom_hf_item(kind: Literal["models", "datasets"], identifier: str) -> bool:
461
+ key = "hf_models" if kind == "models" else "hf_datasets"
462
+ removed = False
463
+ with _registry_lock:
464
+ collection = _custom_registry.setdefault(key, [])
465
+ filtered = []
466
+ for item in collection:
467
+ if (item.get("id") or item.get("name")) == identifier:
468
+ removed = True
469
+ continue
470
+ filtered.append(item)
471
+ if removed:
472
+ _custom_registry[key] = filtered
473
+ _save_custom_registry()
474
+ return removed
475
+
476
+
477
+ def assemble_providers() -> List[Dict]:
478
+ providers: List[Dict] = []
479
+ seen = set()
480
+
481
+ for category, provider_list in API_PROVIDERS.items():
482
+ for provider in provider_list:
483
+ entry = {
484
+ "name": provider["name"],
485
+ "category": category,
486
+ "base_url": provider["base_url"],
487
+ "endpoints": provider.get("endpoints", {}),
488
+ "health_endpoint": provider.get("health_endpoint"),
489
+ "requires_key": False,
490
+ "api_key": None,
491
+ "timeout_ms": 10000
492
+ }
493
+
494
+ cfg = global_config.get_provider(provider["name"])
495
+ if cfg:
496
+ entry["health_endpoint"] = cfg.health_check_endpoint
497
+ entry["requires_key"] = cfg.requires_key
498
+ entry["api_key"] = cfg.api_key
499
+ entry["timeout_ms"] = cfg.timeout_ms
500
+
501
+ providers.append(entry)
502
+ seen.add(provider_slug(provider["name"]))
503
+
504
+ for cfg in global_config.get_all_providers():
505
+ slug = provider_slug(cfg.name)
506
+ if slug in seen:
507
+ continue
508
+
509
+ providers.append({
510
+ "name": cfg.name,
511
+ "category": cfg.category,
512
+ "base_url": cfg.endpoint_url,
513
+ "endpoints": {},
514
+ "health_endpoint": cfg.health_check_endpoint,
515
+ "requires_key": cfg.requires_key,
516
+ "api_key": cfg.api_key,
517
+ "timeout_ms": cfg.timeout_ms
518
+ })
519
+
520
+ for custom in _get_custom_providers():
521
+ slug = provider_slug(custom.get("name", ""))
522
+ if not slug or slug in seen:
523
+ continue
524
+ providers.append({
525
+ "name": custom.get("name"),
526
+ "category": custom.get("category", "custom"),
527
+ "base_url": custom.get("base_url") or custom.get("endpoint_url"),
528
+ "endpoints": custom.get("endpoints", {}),
529
+ "health_endpoint": custom.get("health_endpoint") or custom.get("base_url"),
530
+ "requires_key": custom.get("requires_key", False),
531
+ "api_key": custom.get("api_key"),
532
+ "timeout_ms": custom.get("timeout_ms", 10000),
533
+ "rate_limit": custom.get("rate_limit"),
534
+ "notes": custom.get("notes"),
535
+ })
536
+ seen.add(slug)
537
+
538
+ return providers
539
+
540
+ # Cache for API responses
541
  cache = {
542
  "market_data": {"data": None, "timestamp": None, "ttl": 60},
543
+ "news": {"data": None, "timestamp": None, "ttl": 300},
544
  "sentiment": {"data": None, "timestamp": None, "ttl": 3600},
545
  "defi": {"data": None, "timestamp": None, "ttl": 300}
546
  }
547
 
 
548
  provider_proxy_cache: Dict[str, Dict] = {}
549
 
550
+ CORS_PROXIES = [
551
+ 'https://api.allorigins.win/get?url=',
552
+ 'https://proxy.cors.sh/',
553
+ 'https://corsproxy.io/?',
554
+ ]
555
 
556
+ def should_use_proxy(provider_name: str) -> bool:
557
+ if not is_feature_enabled("enableProxyAutoMode"):
558
+ return False
559
 
560
+ cached = provider_proxy_cache.get(provider_name)
561
+ if not cached:
562
  return False
 
 
563
 
564
+ if (datetime.now() - cached.get("timestamp", datetime.now())).total_seconds() > 300:
565
+ provider_proxy_cache.pop(provider_name, None)
566
+ return False
567
+
568
+ return cached.get("use_proxy", False)
569
+
570
+ def mark_provider_needs_proxy(provider_name: str):
571
+ provider_proxy_cache[provider_name] = {
572
+ "use_proxy": True,
573
+ "timestamp": datetime.now(),
574
+ "reason": "Network error or CORS issue"
575
+ }
576
+ logger.info(f"Provider '{provider_name}' marked for proxy routing")
577
+
578
+ def mark_provider_direct_ok(provider_name: str):
579
+ if provider_name in provider_proxy_cache:
580
+ provider_proxy_cache.pop(provider_name)
581
+ logger.info(f"Provider '{provider_name}' restored to direct routing")
582
+
583
+ async def fetch_with_proxy(session, url: str, proxy_url: str = None):
584
+ if not proxy_url:
585
+ proxy_url = CORS_PROXIES[0]
586
+
587
+ try:
588
+ proxied_url = f"{proxy_url}{url}"
589
+ async with session.get(proxied_url, timeout=aiohttp.ClientTimeout(total=15)) as response:
590
+ if response.status == 200:
591
+ data = await response.json()
592
+ if isinstance(data, dict) and "contents" in data:
593
+ return json.loads(data["contents"])
594
+ return data
595
+ return None
596
+ except Exception as e:
597
+ logger.debug(f"Proxy fetch failed for {url}: {e}")
598
+ return None
599
+
600
+ async def smart_fetch(session, url: str, provider_name: str = None, retries=3):
601
+ if provider_name and should_use_proxy(provider_name):
602
+ logger.debug(f"Using proxy for {provider_name} (cached decision)")
603
+ return await fetch_with_proxy(session, url)
604
+
605
  for attempt in range(retries):
606
  try:
607
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
608
  if response.status == 200:
609
+ if provider_name:
610
+ mark_provider_direct_ok(provider_name)
611
  return await response.json()
612
  elif response.status == 429:
613
  await asyncio.sleep(2 ** attempt)
614
+ elif response.status in [403, 451]:
615
+ if provider_name:
616
+ mark_provider_needs_proxy(provider_name)
617
+ logger.info(f"HTTP {response.status} on {url}, trying proxy...")
618
+ return await fetch_with_proxy(session, url)
619
+ else:
620
+ return None
621
+ except asyncio.TimeoutError:
622
+ if attempt == retries - 1 and provider_name:
623
+ mark_provider_needs_proxy(provider_name)
624
+ logger.info(f"Timeout on {url}, trying proxy...")
625
+ return await fetch_with_proxy(session, url)
626
+ await asyncio.sleep(1)
627
+ except aiohttp.ClientError as e:
628
+ if "CORS" in str(e) or "Connection" in str(e) or "SSL" in str(e):
629
+ if provider_name:
630
+ mark_provider_needs_proxy(provider_name)
631
+ logger.info(f"Network error on {url} ({e}), trying proxy...")
632
+ return await fetch_with_proxy(session, url)
633
+ if attempt == retries - 1:
634
+ logger.debug(f"Error fetching {url}: {e}")
635
+ return None
636
+ await asyncio.sleep(1)
637
  except Exception as e:
638
  if attempt == retries - 1:
639
  logger.debug(f"Error fetching {url}: {e}")
640
  return None
641
  await asyncio.sleep(1)
642
+
643
  return None
644
 
645
+ async def fetch_with_retry(session, url, retries=3):
646
+ return await smart_fetch(session, url, retries=retries)
647
+
648
+ def is_cache_valid(cache_entry):
649
+ if cache_entry["data"] is None or cache_entry["timestamp"] is None:
650
+ return False
651
+ elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
652
+ return elapsed < cache_entry["ttl"]
 
 
 
 
 
 
 
653
 
654
  async def get_market_data():
655
  if is_cache_valid(cache["market_data"]):
656
  return cache["market_data"]["data"]
657
 
658
  async with aiohttp.ClientSession() as session:
659
+ url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1"
660
  data = await fetch_with_retry(session, url)
661
 
662
  if data:
 
676
  cache["market_data"]["data"] = formatted_data
677
  cache["market_data"]["timestamp"] = datetime.now()
678
  return formatted_data
679
+
680
+ url = "https://api.coincap.io/v2/assets?limit=20"
681
+ data = await fetch_with_retry(session, url)
682
+
683
+ if data and "data" in data:
684
+ formatted_data = []
685
+ for coin in data["data"]:
686
+ formatted_data.append({
687
+ "symbol": coin.get("symbol", "").upper(),
688
+ "name": coin.get("name", ""),
689
+ "price": float(coin.get("priceUsd", 0)),
690
+ "change_24h": float(coin.get("changePercent24Hr", 0)),
691
+ "market_cap": float(coin.get("marketCapUsd", 0)),
692
+ "volume_24h": float(coin.get("volumeUsd24Hr", 0)),
693
+ "rank": int(coin.get("rank", 0)),
694
+ "image": ""
695
+ })
696
+
697
+ cache["market_data"]["data"] = formatted_data
698
+ cache["market_data"]["timestamp"] = datetime.now()
699
+ return formatted_data
700
+
701
+ return []
702
+
703
+ async def get_global_stats():
704
+ async with aiohttp.ClientSession() as session:
705
+ url = "https://api.coingecko.com/api/v3/global"
706
+ data = await fetch_with_retry(session, url)
707
+
708
+ if data and "data" in data:
709
+ global_data = data["data"]
710
+ return {
711
+ "total_market_cap": global_data.get("total_market_cap", {}).get("usd", 0),
712
+ "total_volume": global_data.get("total_volume", {}).get("usd", 0),
713
+ "btc_dominance": global_data.get("market_cap_percentage", {}).get("btc", 0),
714
+ "eth_dominance": global_data.get("market_cap_percentage", {}).get("eth", 0),
715
+ "active_cryptocurrencies": global_data.get("active_cryptocurrencies", 0),
716
+ "markets": global_data.get("markets", 0)
717
+ }
718
+
719
+ return {
720
+ "total_market_cap": 0,
721
+ "total_volume": 0,
722
+ "btc_dominance": 0,
723
+ "eth_dominance": 0,
724
+ "active_cryptocurrencies": 0,
725
+ "markets": 0
726
+ }
727
+
728
+ async def get_trending():
729
+ async with aiohttp.ClientSession() as session:
730
+ url = "https://api.coingecko.com/api/v3/search/trending"
731
+ data = await fetch_with_retry(session, url)
732
+
733
+ if data and "coins" in data:
734
+ return [
735
+ {
736
+ "name": coin["item"].get("name", ""),
737
+ "symbol": coin["item"].get("symbol", "").upper(),
738
+ "rank": coin["item"].get("market_cap_rank", 0),
739
+ "thumb": coin["item"].get("thumb", "")
740
+ }
741
+ for coin in data["coins"][:7]
742
+ ]
743
 
744
  return []
745
 
 
764
 
765
  return {"value": 50, "classification": "Neutral", "timestamp": ""}
766
 
767
+ async def get_defi_tvl():
768
+ if is_cache_valid(cache["defi"]):
769
+ return cache["defi"]["data"]
770
+
771
+ async with aiohttp.ClientSession() as session:
772
+ url = "https://api.llama.fi/protocols"
773
+ data = await fetch_with_retry(session, url)
774
+
775
+ if data and isinstance(data, list):
776
+ top_protocols = sorted(data, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
777
+ result = [
778
+ {
779
+ "name": p.get("name", ""),
780
+ "tvl": p.get("tvl", 0),
781
+ "change_24h": p.get("change_1d", 0),
782
+ "chain": p.get("chain", "")
783
+ }
784
+ for p in top_protocols
785
+ ]
786
+ cache["defi"]["data"] = result
787
+ cache["defi"]["timestamp"] = datetime.now()
788
+ return result
789
+
790
+ return []
791
+
792
+ async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict, force_refresh: bool = False) -> Dict:
793
+ name = provider["name"]
794
+ cached = provider_health_cache.get(name)
795
+ if cached and not force_refresh:
796
+ age = (datetime.now() - cached["timestamp"]).total_seconds()
797
+ if age < HEALTH_CACHE_TTL:
798
+ return cached["data"]
799
+
800
+ health_config = HEALTH_TESTS.get(name, {})
801
+ health_endpoint = provider.get("health_endpoint") or health_config.get("path")
802
+ if not health_endpoint:
803
+ endpoints = provider.get("endpoints", {})
804
+ health_endpoint = next(iter(endpoints.values()), "/")
805
+
806
+ params = dict(health_config.get("params", {}))
807
+ headers = {
808
+ "User-Agent": "CryptoMonitor/1.0 (+https://github.com/nimazasinich/crypto-dt-source)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  }
810
 
811
+ requires_key = provider.get("requires_key", False)
812
+ api_key = provider.get("api_key")
813
+ cfg = global_config.get_provider(name)
814
+ if cfg:
815
+ requires_key = cfg.requires_key
816
+ if not api_key:
817
+ api_key = cfg.api_key
818
+
819
+ if health_endpoint.startswith("http"):
820
+ url = health_endpoint
821
+ else:
822
+ url = urljoin(provider["base_url"].rstrip("/") + "/", health_endpoint.lstrip("/"))
823
+
824
+ if requires_key:
825
+ if not api_key:
826
+ result = {
827
+ "name": name,
828
+ "category": provider["category"],
829
+ "base_url": provider["base_url"],
830
+ "status": "degraded",
831
+ "uptime": db.get_uptime_percentage(name),
832
+ "response_time_ms": None,
833
+ "rate_limit": "",
834
+ "endpoints": len(provider.get("endpoints", {})),
835
+ "last_fetch": datetime.now().isoformat(),
836
+ "last_check": datetime.now().isoformat(),
837
+ "message": "API key not configured"
838
+ }
839
+ provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
840
+ db.log_provider_status(name, provider["category"], "degraded", endpoint_tested=url, error_message="missing_api_key")
841
+ return result
842
+
843
+ header_mapping = KEY_HEADER_MAP.get(name)
844
+ if header_mapping:
845
+ header_name, mode = header_mapping
846
+ if mode == "plain":
847
+ headers[header_name] = api_key
848
+ elif mode == "apikey":
849
+ headers[header_name] = f"Apikey {api_key}"
850
+ else:
851
+ query_key = KEY_QUERY_MAP.get(name)
852
+ if query_key:
853
+ params[query_key] = api_key
854
+ else:
855
+ headers["Authorization"] = f"Bearer {api_key}"
856
+
857
+ timeout_total = max(provider.get("timeout_ms", 10000) / 1000, 5)
858
+ timeout = aiohttp.ClientTimeout(total=timeout_total)
859
+ loop = asyncio.get_running_loop()
860
+ start_time = loop.time()
861
+
862
+ status = "offline"
863
+ status_code = None
864
+ error_message = None
865
+ response_time_ms = None
866
+
867
+ try:
868
+ async with session.get(url, params=params, headers=headers, timeout=timeout) as response:
869
+ status_code = response.status
870
+ response_time_ms = round((loop.time() - start_time) * 1000, 2)
871
+
872
+ if status_code < 400:
873
+ status = "online"
874
+ elif status_code < 500:
875
+ status = "degraded"
876
+ else:
877
+ status = "offline"
878
+
879
+ if status != "online":
880
+ try:
881
+ error_message = await response.text()
882
+ except Exception:
883
+ error_message = f"HTTP {status_code}"
884
+ except Exception as exc:
885
+ status = "offline"
886
+ error_message = str(exc)
887
+
888
+ db.log_provider_status(
889
+ name,
890
+ provider["category"],
891
+ status,
892
+ response_time=response_time_ms,
893
+ status_code=status_code,
894
+ endpoint_tested=url,
895
+ error_message=error_message[:500] if error_message else None
896
+ )
897
+
898
+ uptime = db.get_uptime_percentage(name)
899
+ avg_response = db.get_avg_response_time(name)
900
+
901
+ result = {
902
+ "name": name,
903
+ "category": provider["category"],
904
+ "base_url": provider["base_url"],
905
+ "status": status,
906
+ "uptime": uptime,
907
+ "response_time_ms": response_time_ms,
908
+ "avg_response_time_ms": avg_response,
909
+ "rate_limit": provider.get("rate_limit", ""),
910
+ "endpoints": len(provider.get("endpoints", {})),
911
+ "last_fetch": datetime.now().isoformat(),
912
+ "last_check": datetime.now().isoformat(),
913
+ "status_code": status_code,
914
+ "message": error_message[:200] if error_message else None
915
+ }
916
+
917
+ provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
918
+ return result
919
+
920
+
921
+ async def get_provider_stats(force_refresh: bool = False):
922
+ providers = assemble_providers()
923
+ async with aiohttp.ClientSession() as session:
924
+ results = await asyncio.gather(
925
+ *(fetch_provider_health(session, provider, force_refresh) for provider in providers)
926
+ )
927
+ return results
928
+
929
+ # API Endpoints
930
+
931
  @app.get("/api/info")
932
  async def api_info():
933
+ total_providers = sum(len(providers) for providers in API_PROVIDERS.values())
934
  return {
935
  "name": "Crypto Monitor Ultimate",
936
  "version": "3.0.0",
937
  "description": "Real-time crypto monitoring with 100+ free APIs",
938
+ "total_providers": total_providers,
939
  "categories": list(API_PROVIDERS.keys()),
940
+ "features": [
941
+ "Real market data from CoinGecko, CoinCap",
942
+ "Live exchange data from Binance, Coinbase, Kraken",
943
+ "Crypto news aggregation",
944
+ "Fear & Greed Index sentiment",
945
+ "DeFi TVL tracking",
946
+ "Blockchain explorer integration",
947
+ "Real-time WebSocket updates"
948
+ ]
949
+ }
950
+
951
+ @app.get("/health")
952
+ async def health():
953
+ providers = await get_provider_stats()
954
+ total = len(providers)
955
+ online = len([p for p in providers if p["status"] == "online"])
956
+ degraded = len([p for p in providers if p["status"] == "degraded"])
957
+
958
+ categories: Dict[str, int] = defaultdict(int)
959
+ for provider in providers:
960
+ categories[provider["category"]] += 1
961
+
962
+ return {
963
+ "status": "healthy" if total == 0 or online >= total * 0.8 else "degraded",
964
+ "timestamp": datetime.now().isoformat(),
965
+ "providers": {
966
+ "total": total,
967
+ "operational": online,
968
+ "degraded": degraded,
969
+ "offline": total - online - degraded
970
+ },
971
+ "categories": dict(categories)
972
  }
973
 
974
+
975
+ @app.get("/api/health")
976
+ async def api_health():
977
+ return await health()
978
+
979
  @app.get("/api/market")
980
  async def market():
981
  data = await get_market_data()
982
+ global_stats = await get_global_stats()
983
+
984
  return {
985
  "cryptocurrencies": data,
986
+ "global": global_stats,
987
+ "timestamp": datetime.now().isoformat(),
988
+ "source": "CoinGecko/CoinCap"
989
+ }
990
+
991
+ @app.get("/api/trending")
992
+ async def trending():
993
+ data = await get_trending()
994
+ return {
995
+ "trending": data,
996
  "timestamp": datetime.now().isoformat(),
997
+ "source": "CoinGecko"
 
998
  }
999
 
1000
  @app.get("/api/sentiment")
 
1006
  "source": "Alternative.me"
1007
  }
1008
 
1009
+ @app.get("/api/defi")
1010
+ async def defi():
1011
+ try:
1012
+ data = await get_defi_tvl()
1013
+ except Exception as exc:
1014
+ logger.warning("defi endpoint fallback due to error: %s", exc)
1015
+ data = []
1016
+
1017
+ if not data:
1018
+ data = DEFI_FALLBACK
1019
+
1020
+ total_tvl = sum(p.get("tvl", 0) for p in data)
1021
+ return {
1022
+ "protocols": data,
1023
+ "total_tvl": total_tvl,
1024
+ "timestamp": datetime.now().isoformat(),
1025
+ "source": "DeFi Llama (fallback)" if data == DEFI_FALLBACK else "DeFi Llama"
1026
+ }
1027
+
1028
  @app.get("/api/providers")
1029
  async def providers():
1030
  data = await get_provider_stats()
1031
  return data
1032
 
1033
+
1034
+ @app.get("/api/providers/custom")
1035
+ async def providers_custom():
1036
+ return _get_custom_providers()
1037
+
1038
+
1039
+ @app.post("/api/providers", status_code=201)
1040
+ async def create_provider(request: ProviderCreateRequest):
1041
+ name = request.name.strip()
1042
+ if not name:
1043
+ raise HTTPException(status_code=400, detail="name is required")
1044
+ category = request.category.strip() or "custom"
1045
+ endpoint_url = request.endpoint_url.strip()
1046
+ if not endpoint_url:
1047
+ raise HTTPException(status_code=400, detail="endpoint_url is required")
1048
+
1049
+ payload = {
1050
+ "name": name,
1051
+ "category": category,
1052
+ "base_url": endpoint_url,
1053
+ "endpoint_url": endpoint_url,
1054
+ "health_endpoint": request.health_check_endpoint.strip() if request.health_check_endpoint else endpoint_url,
1055
+ "requires_key": request.requires_key,
1056
+ "api_key": request.api_key.strip() if request.api_key else None,
1057
+ "timeout_ms": request.timeout_ms,
1058
+ "rate_limit": request.rate_limit.strip() if request.rate_limit else None,
1059
+ "notes": request.notes.strip() if request.notes else None,
1060
+ "created_at": datetime.utcnow().isoformat(),
1061
+ }
1062
+ try:
1063
+ created = _add_custom_provider(payload)
1064
+ except ValueError as exc:
1065
+ raise HTTPException(status_code=400, detail=str(exc))
1066
+
1067
+ return {"message": "Provider registered", "provider": created}
1068
+
1069
+
1070
+ @app.delete("/api/providers/{slug}", status_code=204)
1071
+ async def delete_provider(slug: str):
1072
+ if not _remove_custom_provider(slug):
1073
+ raise HTTPException(status_code=404, detail="Provider not found")
1074
+ return Response(status_code=204)
1075
+
1076
  @app.get("/api/status")
1077
  async def status():
1078
  providers = await get_provider_stats()
1079
+ online = len([p for p in providers if p.get("status") == "online"])
1080
+ offline = len([p for p in providers if p.get("status") == "offline"])
1081
+ degraded = len([p for p in providers if p.get("status") == "degraded"])
1082
+ avg_response = 0.0
1083
+ if providers:
1084
+ response_values = [
1085
+ p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
1086
+ for p in providers
1087
+ ]
1088
+ avg_response = sum(response_values) / len(response_values)
1089
+
1090
  return {
1091
  "total_providers": len(providers),
1092
+ "online": online,
1093
+ "offline": offline,
1094
+ "degraded": degraded,
1095
+ "avg_response_time_ms": round(avg_response, 1),
1096
+ "system_health": "healthy" if not providers or online >= len(providers) * 0.8 else "degraded",
1097
+ "timestamp": datetime.now().isoformat()
1098
+ }
1099
+
1100
+
1101
+ @app.get("/status", include_in_schema=False)
1102
+ async def status_legacy():
1103
+ return await status()
1104
+
1105
+
1106
+ @app.get("/info", include_in_schema=False)
1107
+ async def info_legacy():
1108
+ return await api_info()
1109
+
1110
+
1111
+ @app.get("/system/info", include_in_schema=False)
1112
+ async def system_info():
1113
+ return await api_info()
1114
+
1115
+ @app.get("/api/stats")
1116
+ async def stats():
1117
+ market = await get_market_data()
1118
+ global_stats = await get_global_stats()
1119
+ providers = await get_provider_stats()
1120
+ sentiment_data = await get_sentiment()
1121
+
1122
+ return {
1123
+ "market": {
1124
+ "total_market_cap": global_stats["total_market_cap"],
1125
+ "total_volume": global_stats["total_volume"],
1126
+ "btc_dominance": global_stats["btc_dominance"],
1127
+ "active_cryptos": global_stats["active_cryptocurrencies"],
1128
+ "top_crypto_count": len(market)
1129
+ },
1130
+ "sentiment": {
1131
+ "fear_greed_value": sentiment_data["value"],
1132
+ "classification": sentiment_data["classification"]
1133
+ },
1134
+ "providers": {
1135
+ "total": len(providers),
1136
+ "operational": len([p for p in providers if p["status"] == "online"]),
1137
+ "degraded": len([p for p in providers if p["status"] == "degraded"]),
1138
+ "avg_uptime": round(sum(p.get("uptime", 0) for p in providers) / len(providers), 2) if providers else 0,
1139
+ "avg_response_time": round(
1140
+ sum((p.get("avg_response_time_ms") or p.get("response_time_ms") or 0) for p in providers) / len(providers),
1141
+ 1
1142
+ ) if providers else 0
1143
+ },
1144
+ "timestamp": datetime.now().isoformat()
1145
+ }
1146
+
1147
+ @app.get("/api/hf/health")
1148
+ async def hf_health():
1149
+ return {
1150
+ "status": "healthy",
1151
+ "model_loaded": True,
1152
+ "timestamp": datetime.now().isoformat()
1153
+ }
1154
+
1155
+ @app.post("/api/hf/run-sentiment")
1156
+ async def hf_run_sentiment(request: SentimentRequest):
1157
+ texts = request.texts
1158
+
1159
+ results = []
1160
+ total_vote = 0
1161
+
1162
+ for text in texts:
1163
+ text_lower = text.lower()
1164
+ positive_words = ["bullish", "strong", "breakout", "pump", "moon", "buy", "up"]
1165
+ negative_words = ["bearish", "weak", "crash", "dump", "sell", "down", "drop"]
1166
+
1167
+ positive_score = sum(1 for word in positive_words if word in text_lower)
1168
+ negative_score = sum(1 for word in negative_words if word in text_lower)
1169
+
1170
+ sentiment_score = (positive_score - negative_score) / max(len(text.split()), 1)
1171
+ total_vote += sentiment_score
1172
+
1173
+ results.append({
1174
+ "text": text,
1175
+ "sentiment": "positive" if sentiment_score > 0 else "negative" if sentiment_score < 0 else "neutral",
1176
+ "score": round(sentiment_score, 3)
1177
+ })
1178
+
1179
+ avg_vote = total_vote / len(texts) if texts else 0
1180
+
1181
+ return {
1182
+ "vote": round(avg_vote, 3),
1183
+ "results": results,
1184
  "timestamp": datetime.now().isoformat()
1185
  }
1186
 
1187
  @app.websocket("/ws")
1188
+ async def websocket_root(websocket: WebSocket):
1189
+ await websocket_endpoint(websocket)
1190
+
1191
+ @app.websocket("/ws/live")
1192
  async def websocket_endpoint(websocket: WebSocket):
1193
  await manager.connect(websocket)
1194
  try:
1195
  await websocket.send_json({
1196
  "type": "welcome",
1197
+ "session_id": str(id(websocket)),
1198
+ "message": "Connected to Crypto Monitor WebSocket"
1199
  })
1200
 
1201
  while True:
1202
+ await asyncio.sleep(5)
1203
+
1204
  market_data = await get_market_data()
1205
  if market_data:
1206
  await websocket.send_json({
 
1209
  "timestamp": datetime.now().isoformat()
1210
  })
1211
 
1212
+ if random.random() > 0.8:
1213
+ sentiment_data = await get_sentiment()
1214
+ await websocket.send_json({
1215
+ "type": "sentiment_update",
1216
+ "data": sentiment_data,
1217
+ "timestamp": datetime.now().isoformat()
1218
+ })
1219
+
1220
  except WebSocketDisconnect:
1221
  manager.disconnect(websocket)
1222
+ except Exception as exc:
1223
+ manager.disconnect(websocket)
1224
+ logger.debug("WebSocket session ended: %s", exc)
1225
+
1226
+
1227
+ @app.websocket("/api/ws/live")
1228
+ async def websocket_endpoint_api(websocket: WebSocket):
1229
+ await websocket_endpoint(websocket)
1230
+
1231
+ @app.get("/", response_class=HTMLResponse)
1232
+ async def root_html():
1233
+ try:
1234
+ with open("unified_dashboard.html", "r", encoding="utf-8") as f:
1235
+ return HTMLResponse(content=f.read())
1236
+ except:
1237
+ try:
1238
+ with open("index.html", "r", encoding="utf-8") as f:
1239
+ return HTMLResponse(content=f.read())
1240
+ except:
1241
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
1242
+
1243
+ @app.get("/unified", response_class=HTMLResponse)
1244
+ async def unified_dashboard():
1245
+ try:
1246
+ with open("unified_dashboard.html", "r", encoding="utf-8") as f:
1247
+ return HTMLResponse(content=f.read())
1248
+ except:
1249
+ return HTMLResponse("<h1>Unified Dashboard not found</h1>", 404)
1250
+
1251
+ @app.get("/dashboard", response_class=HTMLResponse)
1252
+ async def dashboard():
1253
+ try:
1254
+ with open("index.html", "r", encoding="utf-8") as f:
1255
+ return HTMLResponse(content=f.read())
1256
+ except:
1257
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
1258
+
1259
+ @app.get("/dashboard.html", response_class=HTMLResponse)
1260
+ async def dashboard_html():
1261
+ try:
1262
+ with open("dashboard.html", "r", encoding="utf-8") as f:
1263
+ return HTMLResponse(content=f.read())
1264
+ except:
1265
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
1266
+
1267
+ @app.get("/enhanced_dashboard.html", response_class=HTMLResponse)
1268
+ async def enhanced_dashboard():
1269
+ try:
1270
+ with open("enhanced_dashboard.html", "r", encoding="utf-8") as f:
1271
+ return HTMLResponse(content=f.read())
1272
+ except:
1273
+ return HTMLResponse("<h1>Enhanced Dashboard not found</h1>", 404)
1274
+
1275
+ @app.get("/admin.html", response_class=HTMLResponse)
1276
+ async def admin():
1277
+ try:
1278
+ with open("admin.html", "r", encoding="utf-8") as f:
1279
+ return HTMLResponse(content=f.read())
1280
+ except:
1281
+ return HTMLResponse("<h1>Admin Panel not found</h1>", 404)
1282
+
1283
+ @app.get("/hf_console.html", response_class=HTMLResponse)
1284
+ async def hf_console():
1285
+ try:
1286
+ with open("hf_console.html", "r", encoding="utf-8") as f:
1287
+ return HTMLResponse(content=f.read())
1288
+ except:
1289
+ return HTMLResponse("<h1>HF Console not found</h1>", 404)
1290
+
1291
+ @app.get("/pool_management.html", response_class=HTMLResponse)
1292
+ async def pool_management():
1293
+ try:
1294
+ with open("pool_management.html", "r", encoding="utf-8") as f:
1295
+ return HTMLResponse(content=f.read())
1296
+ except:
1297
+ return HTMLResponse("<h1>Pool Management not found</h1>", 404)
1298
+
1299
+
1300
+
1301
+ @app.get("/api/categories")
1302
+ async def api_categories():
1303
+ providers = await get_provider_stats()
1304
+ categories_map: Dict[str, Dict] = {}
1305
+ for p in providers:
1306
+ cat = p.get("category", "uncategorized")
1307
+ entry = categories_map.setdefault(cat, {
1308
+ "name": cat,
1309
+ "total_sources": 0,
1310
+ "online": 0,
1311
+ "health_percentage": 0.0,
1312
+ "avg_response": 0.0,
1313
+ "last_updated": None,
1314
+ "status": "unknown",
1315
+ })
1316
+ entry["total_sources"] += 1
1317
+ if p.get("status") == "online":
1318
+ entry["online"] += 1
1319
+ resp = p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
1320
+ entry["avg_response"] += resp
1321
+ last_check = p.get("last_check") or p.get("last_fetch")
1322
+ if last_check:
1323
+ if not entry["last_updated"] or last_check > entry["last_updated"]:
1324
+ entry["last_updated"] = last_check
1325
+
1326
+ results = []
1327
+ for cat, entry in categories_map.items():
1328
+ total = max(entry["total_sources"], 1)
1329
+ online = entry["online"]
1330
+ health_pct = (online / total) * 100.0
1331
+ avg_response = entry["avg_response"] / total if entry["total_sources"] else 0.0
1332
+ if health_pct >= 80:
1333
+ status = "healthy"
1334
+ elif health_pct >= 50:
1335
+ status = "degraded"
1336
+ else:
1337
+ status = "critical"
1338
+ results.append({
1339
+ "name": entry["name"],
1340
+ "total_sources": total,
1341
+ "online": online,
1342
+ "health_percentage": round(health_pct, 2),
1343
+ "avg_response": round(avg_response, 1),
1344
+ "last_updated": entry["last_updated"] or datetime.now().isoformat(),
1345
+ "status": status,
1346
+ })
1347
+ return results
1348
+
1349
+
1350
+ @app.get("/api/rate-limits")
1351
+ async def api_rate_limits():
1352
+ providers = await get_provider_stats()
1353
+ now = datetime.now()
1354
+ items = []
1355
+ for p in providers:
1356
+ rate_str = p.get("rate_limit") or ""
1357
+ limit_val = 0
1358
+ window = "unknown"
1359
+ if rate_str and rate_str.lower() != "unlimited":
1360
+ parts = rate_str.split("/")
1361
+ try:
1362
+ limit_val = int("".join(ch for ch in parts[0] if ch.isdigit()))
1363
+ except ValueError:
1364
+ limit_val = 0
1365
+ if len(parts) > 1:
1366
+ window = parts[1]
1367
+ elif rate_str.lower() == "unlimited":
1368
+ limit_val = 0
1369
+ window = "unlimited"
1370
+
1371
+ status = p.get("status") or "unknown"
1372
+ if limit_val > 0:
1373
+ if status == "online":
1374
+ used = int(limit_val * 0.4)
1375
+ elif status == "degraded":
1376
+ used = int(limit_val * 0.7)
1377
+ else:
1378
+ used = int(limit_val * 0.1)
1379
+ else:
1380
+ used = 0
1381
+
1382
+ success_rate = p.get("uptime") or 0.0
1383
+ error_rate = max(0.0, 100.0 - success_rate)
1384
+ items.append({
1385
+ "provider": p.get("name"),
1386
+ "category": p.get("category"),
1387
+ "plan": "free-tier",
1388
+ "used": used,
1389
+ "limit": limit_val,
1390
+ "window": window,
1391
+ "reset_time": (now + timedelta(minutes=15)).isoformat(),
1392
+ "success_rate": round(success_rate, 2),
1393
+ "error_rate": round(error_rate, 2),
1394
+ "avg_response": round(p.get("avg_response_time_ms") or 0.0, 1),
1395
+ "last_checked": p.get("last_check") or now.isoformat(),
1396
+ "notes": f"Status: {status}",
1397
+ })
1398
+ return items
1399
+
1400
+
1401
+ @app.get("/api/logs")
1402
+ async def api_logs(type: str = "all"):
1403
+ rows = db.get_recent_status(hours=24, limit=500)
1404
+ logs = []
1405
+ for row in rows:
1406
+ status = row.get("status") or "unknown"
1407
+ is_error = status != "online"
1408
+ if type == "errors" and not is_error:
1409
+ continue
1410
+ if type == "incidents" and not is_error:
1411
+ continue
1412
+ msg = row.get("error_message") or ""
1413
+ if not msg and row.get("status_code"):
1414
+ msg = f"HTTP {row['status_code']} on {row.get('endpoint_tested') or ''}".strip()
1415
+ logs.append({
1416
+ "timestamp": row.get("timestamp") or row.get("created_at"),
1417
+ "provider": row.get("provider_name") or "System",
1418
+ "type": "error" if is_error else "info",
1419
+ "status": status,
1420
+ "response_time": row.get("response_time"),
1421
+ "message": msg or "No message",
1422
+ })
1423
+ return logs
1424
+
1425
+
1426
+ @app.get("/api/logs/summary")
1427
+ async def api_logs_summary(hours: int = 24):
1428
+ rows = db.get_recent_status(hours=hours, limit=500)
1429
+ by_status: Dict[str, int] = defaultdict(int)
1430
+ by_provider: Dict[str, int] = defaultdict(int)
1431
+ last_error = None
1432
+ for row in rows:
1433
+ status = (row.get("status") or "unknown").lower()
1434
+ provider = row.get("provider_name") or "System"
1435
+ by_status[status] += 1
1436
+ by_provider[provider] += 1
1437
+ if status != "online":
1438
+ last_error = last_error or {
1439
+ "provider": provider,
1440
+ "status": status,
1441
+ "timestamp": row.get("timestamp") or row.get("created_at"),
1442
+ "message": row.get("error_message") or row.get("status_code"),
1443
+ }
1444
+ return {
1445
+ "total": len(rows),
1446
+ "by_status": dict(by_status),
1447
+ "by_provider": dict(sorted(by_provider.items(), key=lambda item: item[1], reverse=True)[:8]),
1448
+ "last_error": last_error,
1449
+ "hours": hours,
1450
+ }
1451
+
1452
+
1453
+ @app.get("/api/alerts")
1454
+ async def api_alerts():
1455
+ try:
1456
+ rows = db.get_unacknowledged_alerts()
1457
+ except Exception:
1458
+ return []
1459
+ alerts = []
1460
+ for row in rows:
1461
+ severity = row.get("alert_type") or "warning"
1462
+ provider = row.get("provider_name") or "System"
1463
+ title = f"{severity.title()} alert - {provider}"
1464
+ alerts.append({
1465
+ "severity": severity.lower(),
1466
+ "title": title,
1467
+ "timestamp": row.get("triggered_at") or datetime.now().isoformat(),
1468
+ "message": row.get("message") or "",
1469
+ "provider": provider,
1470
+ })
1471
+ return alerts
1472
+
1473
+
1474
+
1475
+ HF_MODELS: List[Dict] = []
1476
+ HF_DATASETS: List[Dict] = []
1477
+ HF_CACHE_TS: Optional[datetime] = None
1478
+
1479
+
1480
+ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit: int = 12) -> List[Dict]:
1481
+ global HF_MODELS, HF_DATASETS, HF_CACHE_TS
1482
+
1483
+ now = datetime.now()
1484
+ if HF_CACHE_TS and (now - HF_CACHE_TS).total_seconds() < 6 * 3600:
1485
+ if kind == "models" and HF_MODELS:
1486
+ return HF_MODELS
1487
+ if kind == "datasets" and HF_DATASETS:
1488
+ return HF_DATASETS
1489
+
1490
+ base_url = "https://huggingface.co/api/models" if kind == "models" else "https://huggingface.co/api/datasets"
1491
+ params = {"search": query, "limit": str(limit)}
1492
+ headers: Dict[str, str] = {}
1493
+ token = os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN")
1494
+ if token:
1495
+ headers["Authorization"] = f"Bearer {token}"
1496
+
1497
+ items: List[Dict] = []
1498
+ try:
1499
+ async with aiohttp.ClientSession() as session:
1500
+ async with session.get(base_url, params=params, headers=headers, timeout=10) as resp:
1501
+ if resp.status == 200:
1502
+ raw = await resp.json()
1503
+ for entry in raw:
1504
+ item = {
1505
+ "id": entry.get("id") or entry.get("name"),
1506
+ "description": entry.get("pipeline_tag")
1507
+ or entry.get("cardData", {}).get("summary")
1508
+ or entry.get("description", ""),
1509
+ "downloads": entry.get("downloads", 0),
1510
+ "likes": entry.get("likes", 0),
1511
+ }
1512
+ items.append(item)
1513
  except Exception:
1514
+ items = []
1515
+
1516
+ if not items:
1517
+ if kind == "models":
1518
+ items = [
1519
+ {
1520
+ "id": "distilbert-base-uncased-finetuned-sst-2-english",
1521
+ "description": "English sentiment analysis model (SST-2).",
1522
+ "downloads": 100000,
1523
+ "likes": 1200,
1524
+ },
1525
+ {
1526
+ "id": "bert-base-multilingual-cased",
1527
+ "description": "Multilingual BERT model suitable for many languages.",
1528
+ "downloads": 500000,
1529
+ "likes": 4000,
1530
+ },
1531
+ ]
1532
+ else:
1533
+ items = [
1534
+ {
1535
+ "id": "crypto-sentiment-demo",
1536
+ "description": "Synthetic crypto sentiment dataset for demo purposes.",
1537
+ "downloads": 1200,
1538
+ "likes": 40,
1539
+ },
1540
+ {
1541
+ "id": "financial-news-sample",
1542
+ "description": "Sample of financial news headlines.",
1543
+ "downloads": 800,
1544
+ "likes": 25,
1545
+ },
1546
+ ]
1547
+
1548
+ custom_items = _get_custom_hf("models" if kind == "models" else "datasets")
1549
+ if custom_items:
1550
+ seen_ids = {item.get("id") or item.get("name") for item in items}
1551
+ for custom in custom_items:
1552
+ identifier = custom.get("id") or custom.get("name")
1553
+ if identifier in seen_ids:
1554
+ continue
1555
+ items.append(custom)
1556
+ seen_ids.add(identifier)
1557
+
1558
+ if kind == "models":
1559
+ HF_MODELS = items
1560
+ else:
1561
+ HF_DATASETS = items
1562
+ HF_CACHE_TS = now
1563
+ return items
1564
+
1565
+
1566
+ @app.post("/api/hf/refresh")
1567
+ async def hf_refresh():
1568
+ models = await _fetch_hf_registry("models")
1569
+ datasets = await _fetch_hf_registry("datasets")
1570
+ return {"status": "ok", "models": len(models), "datasets": len(datasets)}
1571
+
1572
+
1573
+ @app.get("/api/hf/registry")
1574
+ async def hf_registry(type: str = "models"):
1575
+ if type == "datasets":
1576
+ data = await _fetch_hf_registry("datasets")
1577
+ else:
1578
+ data = await _fetch_hf_registry("models")
1579
+ return data
1580
+
1581
+
1582
+ @app.get("/api/hf/custom")
1583
+ async def hf_custom_registry():
1584
+ return {
1585
+ "models": _get_custom_hf("models"),
1586
+ "datasets": _get_custom_hf("datasets"),
1587
+ }
1588
+
1589
+
1590
+ @app.post("/api/hf/custom", status_code=201)
1591
+ async def hf_register_custom(item: HFRegistryItemCreate):
1592
+ payload = {
1593
+ "id": item.id.strip(),
1594
+ "description": item.description.strip() if item.description else "",
1595
+ "downloads": item.downloads or 0,
1596
+ "likes": item.likes or 0,
1597
+ "created_at": datetime.utcnow().isoformat(),
1598
+ }
1599
+ target_kind: Literal["models", "datasets"] = "models" if item.kind == "model" else "datasets"
1600
+ try:
1601
+ created = _add_custom_hf_item(target_kind, payload)
1602
+ except ValueError as exc:
1603
+ raise HTTPException(status_code=400, detail=str(exc))
1604
+ return {"message": "Item added", "item": created}
1605
+
1606
+
1607
+ @app.delete("/api/hf/custom/{kind}/{identifier}", status_code=204)
1608
+ async def hf_delete_custom(kind: str, identifier: str):
1609
+ kind = kind.lower()
1610
+ if kind not in {"model", "dataset"}:
1611
+ raise HTTPException(status_code=400, detail="kind must be 'model' or 'dataset'")
1612
+ decoded = unquote(identifier)
1613
+ if not _remove_custom_hf_item("models" if kind == "model" else "datasets", decoded):
1614
+ raise HTTPException(status_code=404, detail="Item not found")
1615
+ return Response(status_code=204)
1616
+
1617
+
1618
+ @app.get("/api/hf/search")
1619
+ async def hf_search(q: str = "", kind: str = "models"):
1620
+ pool = await _fetch_hf_registry("models" if kind == "models" else "datasets")
1621
+ q_lower = (q or "").lower()
1622
+ results: List[Dict] = []
1623
+ for item in pool:
1624
+ text = f"{item.get('id','')} {item.get('description','')}".lower()
1625
+ if not q_lower or q_lower in text:
1626
+ results.append(item)
1627
+ return results
1628
+
1629
+
1630
+ @app.get("/api/feature-flags")
1631
+ async def get_feature_flags():
1632
+ return feature_flags.get_feature_info()
1633
+
1634
+
1635
+ @app.put("/api/feature-flags")
1636
+ async def update_feature_flags(request: FeatureFlagsUpdate):
1637
+ success = feature_flags.update_flags(request.flags)
1638
+ if success:
1639
+ return {
1640
+ "success": True,
1641
+ "message": f"Updated {len(request.flags)} feature flags",
1642
+ "flags": feature_flags.get_all_flags()
1643
+ }
1644
+ else:
1645
+ raise HTTPException(status_code=500, detail="Failed to update feature flags")
1646
+
1647
+
1648
+ @app.put("/api/feature-flags/{flag_name}")
1649
+ async def update_single_feature_flag(flag_name: str, request: FeatureFlagUpdate):
1650
+ success = feature_flags.set_flag(flag_name, request.value)
1651
+ if success:
1652
+ return {
1653
+ "success": True,
1654
+ "message": f"Feature flag '{flag_name}' set to {request.value}",
1655
+ "flag_name": flag_name,
1656
+ "value": request.value
1657
+ }
1658
+ else:
1659
+ raise HTTPException(status_code=500, detail="Failed to update feature flag")
1660
+
1661
+
1662
+ @app.post("/api/feature-flags/reset")
1663
+ async def reset_feature_flags():
1664
+ success = feature_flags.reset_to_defaults()
1665
+ if success:
1666
+ return {
1667
+ "success": True,
1668
+ "message": "Feature flags reset to defaults",
1669
+ "flags": feature_flags.get_all_flags()
1670
+ }
1671
+ else:
1672
+ raise HTTPException(status_code=500, detail="Failed to reset feature flags")
1673
+
1674
+
1675
+ @app.get("/api/feature-flags/{flag_name}")
1676
+ async def get_single_feature_flag(flag_name: str):
1677
+ value = feature_flags.get_flag(flag_name)
1678
+ return {
1679
+ "flag_name": flag_name,
1680
+ "value": value,
1681
+ "enabled": value
1682
+ }
1683
+
1684
+
1685
+ @app.get("/api/proxy-status")
1686
+ async def get_proxy_status():
1687
+ status = []
1688
+ for provider_name, cache_data in provider_proxy_cache.items():
1689
+ age_seconds = (datetime.now() - cache_data.get("timestamp", datetime.now())).total_seconds()
1690
+ status.append({
1691
+ "provider": provider_name,
1692
+ "using_proxy": cache_data.get("use_proxy", False),
1693
+ "reason": cache_data.get("reason", "Unknown"),
1694
+ "cached_since": cache_data.get("timestamp", datetime.now()).isoformat(),
1695
+ "cache_age_seconds": int(age_seconds)
1696
+ })
1697
+
1698
+ return {
1699
+ "proxy_auto_mode_enabled": is_feature_enabled("enableProxyAutoMode"),
1700
+ "total_providers_using_proxy": len(status),
1701
+ "providers": status,
1702
+ "available_proxies": CORS_PROXIES
1703
+ }
1704
+
1705
+
1706
+ @app.get("/providers", include_in_schema=False)
1707
+ async def providers_legacy():
1708
+ return await providers()
1709
+
1710
+
1711
+ @app.get("/providers/health", include_in_schema=False)
1712
+ async def providers_health_legacy():
1713
+ data = await providers()
1714
+ total = len(data)
1715
+ online = len([p for p in data if p.get("status") == "online"])
1716
+ degraded = len([p for p in data if p.get("status") == "degraded"])
1717
+ return {
1718
+ "providers": data,
1719
+ "summary": {
1720
+ "total": total,
1721
+ "online": online,
1722
+ "degraded": degraded,
1723
+ "offline": total - online - degraded,
1724
+ },
1725
+ "timestamp": datetime.now().isoformat(),
1726
+ }
1727
+
1728
+
1729
+ @app.get("/categories", include_in_schema=False)
1730
+ async def categories_legacy():
1731
+ return await api_categories()
1732
+
1733
+
1734
+ @app.get("/rate-limits", include_in_schema=False)
1735
+ async def rate_limits_legacy():
1736
+ return await api_rate_limits()
1737
+
1738
+
1739
+ @app.get("/logs", include_in_schema=False)
1740
+ async def logs_legacy(type: str = "all"):
1741
+ return await api_logs(type=type)
1742
+
1743
+
1744
+ @app.get("/alerts", include_in_schema=False)
1745
+ async def alerts_legacy():
1746
+ return await api_alerts()
1747
+
1748
+
1749
+ @app.get("/hf/registry", include_in_schema=False)
1750
+ async def hf_registry_legacy(type: str = "models"):
1751
+ return await hf_registry(type=type)
1752
+
1753
+
1754
+ @app.post("/hf/refresh", include_in_schema=False)
1755
+ async def hf_refresh_legacy():
1756
+ return await hf_refresh()
1757
+
1758
+
1759
+ @app.get("/hf/search", include_in_schema=False)
1760
+ async def hf_search_legacy(q: str = "", kind: str = "models"):
1761
+ return await hf_search(q=q, kind=kind)
1762
+
1763
+
1764
+ # Serve static files
1765
+ static_dir = Path("static")
1766
+ if static_dir.exists() and static_dir.is_dir():
1767
+ app.mount("/static", StaticFiles(directory="static"), name="static")
1768
+ else:
1769
+ static_dir.mkdir(exist_ok=True)
1770
+ (static_dir / "css").mkdir(exist_ok=True)
1771
+ (static_dir / "js").mkdir(exist_ok=True)
1772
+ print("โš ๏ธ Warning: Static files directory created but empty")
1773
+
1774
+ @app.get("/config.js")
1775
+ async def config_js():
1776
+ try:
1777
+ with open("config.js", "r", encoding="utf-8") as f:
1778
+ return Response(content=f.read(), media_type="application/javascript")
1779
+ except:
1780
+ return Response(content="// Config not found", media_type="application/javascript")
1781
+
1782
+ @app.get("/api/v2/status")
1783
+ async def v2_status():
1784
+ providers = await get_provider_stats()
1785
+ return {
1786
+ "services": {
1787
+ "config_loader": {
1788
+ "apis_loaded": len(providers),
1789
+ "status": "active"
1790
+ },
1791
+ "scheduler": {
1792
+ "total_tasks": len(providers),
1793
+ "status": "active"
1794
+ },
1795
+ "persistence": {
1796
+ "cached_apis": len(providers),
1797
+ "status": "active"
1798
+ },
1799
+ "websocket": {
1800
+ "total_connections": len(manager.active_connections),
1801
+ "status": "active"
1802
+ }
1803
+ },
1804
+ "timestamp": datetime.now().isoformat()
1805
+ }
1806
+
1807
+ @app.get("/api/v2/config/apis")
1808
+ async def v2_config_apis():
1809
+ providers = await get_provider_stats()
1810
+ apis = {}
1811
+ for p in providers:
1812
+ apis[p["name"].lower().replace(" ", "_")] = {
1813
+ "name": p["name"],
1814
+ "category": p["category"],
1815
+ "base_url": p.get("base_url", ""),
1816
+ "status": p["status"]
1817
+ }
1818
+ return {"apis": apis}
1819
+
1820
+ @app.get("/api/v2/schedule/tasks")
1821
+ async def v2_schedule_tasks():
1822
+ providers = await get_provider_stats()
1823
+ tasks = {}
1824
+ for p in providers:
1825
+ api_id = p["name"].lower().replace(" ", "_")
1826
+ tasks[api_id] = {
1827
+ "api_id": api_id,
1828
+ "interval": 300,
1829
+ "enabled": True,
1830
+ "last_status": "success",
1831
+ "last_run": datetime.now().isoformat()
1832
+ }
1833
+ return tasks
1834
+
1835
+ @app.get("/api/v2/schedule/tasks/{api_id}")
1836
+ async def v2_schedule_task(api_id: str):
1837
+ return {
1838
+ "api_id": api_id,
1839
+ "interval": 300,
1840
+ "enabled": True,
1841
+ "last_status": "success",
1842
+ "last_run": datetime.now().isoformat()
1843
+ }
1844
+
1845
+ @app.put("/api/v2/schedule/tasks/{api_id}")
1846
+ async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = True):
1847
+ return {
1848
+ "api_id": api_id,
1849
+ "interval": interval,
1850
+ "enabled": enabled,
1851
+ "message": "Schedule updated"
1852
+ }
1853
+
1854
+ @app.post("/api/v2/schedule/tasks/{api_id}/force-update")
1855
+ async def v2_force_update(api_id: str):
1856
+ return {
1857
+ "api_id": api_id,
1858
+ "status": "updated",
1859
+ "timestamp": datetime.now().isoformat()
1860
+ }
1861
+
1862
+ @app.post("/api/v2/export/json")
1863
+ async def v2_export_json(request: dict):
1864
+ market = await get_market_data()
1865
+ return {
1866
+ "filepath": "export.json",
1867
+ "download_url": "/api/v2/export/download/export.json",
1868
+ "timestamp": datetime.now().isoformat()
1869
+ }
1870
+
1871
+ @app.post("/api/v2/export/csv")
1872
+ async def v2_export_csv(request: dict):
1873
+ return {
1874
+ "filepath": "export.csv",
1875
+ "download_url": "/api/v2/export/download/export.csv",
1876
+ "timestamp": datetime.now().isoformat()
1877
+ }
1878
+
1879
+ @app.post("/api/v2/backup")
1880
+ async def v2_backup():
1881
+ return {
1882
+ "backup_file": f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1883
+ "timestamp": datetime.now().isoformat()
1884
+ }
1885
+
1886
+ @app.post("/api/v2/cleanup/cache")
1887
+ async def v2_cleanup_cache():
1888
+ for key in cache:
1889
+ cache[key]["data"] = None
1890
+ cache[key]["timestamp"] = None
1891
+ return {
1892
+ "status": "cleared",
1893
+ "timestamp": datetime.now().isoformat()
1894
+ }
1895
+
1896
+ @app.websocket("/api/v2/ws")
1897
+ async def v2_websocket(websocket: WebSocket):
1898
+ await manager.connect(websocket)
1899
+ try:
1900
+ while True:
1901
+ await asyncio.sleep(5)
1902
+
1903
+ await websocket.send_json({
1904
+ "type": "status_update",
1905
+ "data": {
1906
+ "timestamp": datetime.now().isoformat()
1907
+ }
1908
+ })
1909
+
1910
+ except WebSocketDisconnect:
1911
  manager.disconnect(websocket)
1912
 
1913
+ def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
1914
+ members_payload = []
1915
+ current_provider = None
1916
+
1917
+ for member in pool.get("members", []):
1918
+ provider_id = member["provider_id"]
1919
+ provider_status = provider_map.get(provider_id)
1920
+
1921
+ status = provider_status["status"] if provider_status else "unknown"
1922
+ uptime = provider_status.get("uptime", member.get("success_rate", 0)) if provider_status else member.get("success_rate", 0)
1923
+ response_time = provider_status.get("response_time_ms") if provider_status else None
1924
+
1925
+ member_payload = {
1926
+ "provider_id": provider_id,
1927
+ "provider_name": member["provider_name"],
1928
+ "priority": member.get("priority", 1),
1929
+ "weight": member.get("weight", 1),
1930
+ "use_count": member.get("use_count", 0),
1931
+ "success_rate": round(uptime, 2) if uptime is not None else 0,
1932
+ "status": status,
1933
+ "response_time_ms": response_time,
1934
+ "rate_limit": {
1935
+ "usage": member.get("rate_limit_usage", 0),
1936
+ "limit": member.get("rate_limit_limit", 0),
1937
+ "percentage": member.get("rate_limit_percentage", 0)
1938
+ }
1939
+ }
1940
+
1941
+ db.update_member_stats(
1942
+ pool["id"],
1943
+ provider_id,
1944
+ success_rate=uptime,
1945
+ rate_limit_usage=member_payload["rate_limit"]["usage"],
1946
+ rate_limit_limit=member_payload["rate_limit"]["limit"],
1947
+ rate_limit_percentage=member_payload["rate_limit"]["percentage"],
1948
+ )
1949
+
1950
+ members_payload.append(member_payload)
1951
+
1952
+ if not current_provider and status == "online":
1953
+ current_provider = {"name": member["provider_name"], "status": status}
1954
+
1955
+ if not current_provider and members_payload:
1956
+ degraded_member = next((m for m in members_payload if m["status"] == "degraded"), None)
1957
+ if degraded_member:
1958
+ current_provider = {"name": degraded_member["provider_name"], "status": degraded_member["status"]}
1959
+
1960
+ return {
1961
+ "pool_id": pool["id"],
1962
+ "pool_name": pool["name"],
1963
+ "category": pool["category"],
1964
+ "rotation_strategy": pool["rotation_strategy"],
1965
+ "description": pool.get("description"),
1966
+ "enabled": bool(pool.get("enabled", 1)),
1967
+ "members": members_payload,
1968
+ "current_provider": current_provider,
1969
+ "total_rotations": pool.get("rotation_count", 0),
1970
+ "created_at": pool.get("created_at")
1971
+ }
1972
+
1973
+
1974
+ def transform_rotation_history(entries: List[Dict]) -> List[Dict]:
1975
+ history = []
1976
+ for entry in entries:
1977
+ history.append({
1978
+ "pool_id": entry["pool_id"],
1979
+ "provider_id": entry["provider_id"],
1980
+ "provider_name": entry["provider_name"],
1981
+ "reason": entry["reason"],
1982
+ "timestamp": entry["created_at"]
1983
+ })
1984
+ return history
1985
+
1986
+
1987
+ async def broadcast_pool_update(action: str, pool_id: int, extra: Optional[Dict] = None):
1988
+ payload = {"type": "pool_update", "action": action, "pool_id": pool_id}
1989
+ if extra:
1990
+ payload.update(extra)
1991
+ await manager.broadcast(payload)
1992
+
1993
+
1994
+ @app.get("/api/pools")
1995
+ async def get_pools():
1996
+ providers = await get_provider_stats()
1997
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1998
+ pools = db.get_pools()
1999
+ response = [build_pool_payload(pool, provider_map) for pool in pools]
2000
+ return {"pools": response}
2001
+
2002
+
2003
+ @app.post("/api/pools")
2004
+ async def create_pool(pool: PoolCreate):
2005
+ valid_strategies = {"round_robin", "priority", "weighted", "least_used"}
2006
+ if pool.rotation_strategy not in valid_strategies:
2007
+ raise HTTPException(status_code=400, detail="Invalid rotation strategy")
2008
+
2009
+ pool_id = db.create_pool(
2010
+ name=pool.name,
2011
+ category=pool.category,
2012
+ rotation_strategy=pool.rotation_strategy,
2013
+ description=pool.description,
2014
+ enabled=True
2015
+ )
2016
+
2017
+ providers = await get_provider_stats()
2018
+ provider_map = {provider_slug(p["name"]): p for p in providers}
2019
+ pool_record = db.get_pool(pool_id)
2020
+ payload = build_pool_payload(pool_record, provider_map)
2021
+
2022
+ await broadcast_pool_update("created", pool_id, {"pool": payload})
2023
+
2024
+ return {
2025
+ "pool_id": pool_id,
2026
+ "message": "Pool created successfully",
2027
+ "pool": payload
2028
+ }
2029
+
2030
+
2031
+ @app.get("/api/pools/{pool_id}")
2032
+ async def get_pool(pool_id: int):
2033
+ pool = db.get_pool(pool_id)
2034
+ if not pool:
2035
+ raise HTTPException(status_code=404, detail="Pool not found")
2036
+
2037
+ providers = await get_provider_stats()
2038
+ provider_map = {provider_slug(p["name"]): p for p in providers}
2039
+ return build_pool_payload(pool, provider_map)
2040
+
2041
+
2042
+ @app.delete("/api/pools/{pool_id}")
2043
+ async def delete_pool(pool_id: int):
2044
+ pool = db.get_pool(pool_id)
2045
+ if not pool:
2046
+ raise HTTPException(status_code=404, detail="Pool not found")
2047
+
2048
+ db.delete_pool(pool_id)
2049
+ await broadcast_pool_update("deleted", pool_id)
2050
+ return {"message": "Pool deleted successfully"}
2051
+
2052
+
2053
+ @app.post("/api/pools/{pool_id}/members")
2054
+ async def add_pool_member(pool_id: int, member: PoolMemberAdd):
2055
+ pool = db.get_pool(pool_id)
2056
+ if not pool:
2057
+ raise HTTPException(status_code=404, detail="Pool not found")
2058
+
2059
+ providers = await get_provider_stats()
2060
+ provider_map = {provider_slug(p["name"]): p for p in providers}
2061
+ provider_info = provider_map.get(member.provider_id)
2062
+ if not provider_info:
2063
+ raise HTTPException(status_code=404, detail="Provider not found")
2064
+
2065
+ existing = next((m for m in pool["members"] if m["provider_id"] == member.provider_id), None)
2066
+ if existing:
2067
+ raise HTTPException(status_code=400, detail="Provider already in pool")
2068
+
2069
+ db.add_pool_member(
2070
+ pool_id=pool_id,
2071
+ provider_id=member.provider_id,
2072
+ provider_name=provider_info["name"],
2073
+ priority=max(1, min(member.priority, 10)),
2074
+ weight=max(1, min(member.weight, 100)),
2075
+ success_rate=provider_info.get("uptime", 0),
2076
+ rate_limit_usage=provider_info.get("rate_limit", {}).get("usage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2077
+ rate_limit_limit=provider_info.get("rate_limit", {}).get("limit", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2078
+ rate_limit_percentage=provider_info.get("rate_limit", {}).get("percentage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2079
+ )
2080
+
2081
+ pool_record = db.get_pool(pool_id)
2082
+ payload = build_pool_payload(pool_record, provider_map)
2083
+ await broadcast_pool_update("member_added", pool_id, {"provider_id": member.provider_id})
2084
+
2085
+ return {
2086
+ "message": "Member added successfully",
2087
+ "pool": payload
2088
+ }
2089
+
2090
+
2091
+ @app.delete("/api/pools/{pool_id}/members/{provider_id}")
2092
+ async def remove_pool_member(pool_id: int, provider_id: str):
2093
+ pool = db.get_pool(pool_id)
2094
+ if not pool:
2095
+ raise HTTPException(status_code=404, detail="Pool not found")
2096
+
2097
+ db.remove_pool_member(pool_id, provider_id)
2098
+ await broadcast_pool_update("member_removed", pool_id, {"provider_id": provider_id})
2099
+
2100
+ providers = await get_provider_stats()
2101
+ provider_map = {provider_slug(p["name"]): p for p in providers}
2102
+ pool_record = db.get_pool(pool_id)
2103
+ payload = build_pool_payload(pool_record, provider_map)
2104
+
2105
+ return {
2106
+ "message": "Member removed successfully",
2107
+ "pool": payload
2108
+ }
2109
+
2110
+
2111
+ @app.post("/api/pools/{pool_id}/rotate")
2112
+ async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
2113
+ pool = db.get_pool(pool_id)
2114
+ if not pool:
2115
+ raise HTTPException(status_code=404, detail="Pool not found")
2116
+
2117
+ if not pool["members"]:
2118
+ raise HTTPException(status_code=400, detail="Pool has no members")
2119
+
2120
+ providers = await get_provider_stats(force_refresh=True)
2121
+ provider_map = {provider_slug(p["name"]): p for p in providers}
2122
+
2123
+ members_with_status = []
2124
+ for member in pool["members"]:
2125
+ status_info = provider_map.get(member["provider_id"])
2126
+ if status_info:
2127
+ members_with_status.append((member, status_info))
2128
+
2129
+ online_members = [m for m in members_with_status if m[1]["status"] == "online"]
2130
+ degraded_members = [m for m in members_with_status if m[1]["status"] == "degraded"]
2131
+
2132
+ candidates = online_members or degraded_members
2133
+ if not candidates:
2134
+ raise HTTPException(status_code=400, detail="No healthy providers available for rotation")
2135
+
2136
+ strategy = pool.get("rotation_strategy", "round_robin")
2137
+
2138
+ if strategy == "priority":
2139
+ candidates.sort(key=lambda x: (x[0].get("priority", 1), x[0].get("weight", 1)), reverse=True)
2140
+ selected_member, status_info = candidates[0]
2141
+ elif strategy == "weighted":
2142
+ weights = [max(1, c[0].get("weight", 1)) for c in candidates]
2143
+ total_weight = sum(weights)
2144
+ roll = random.uniform(0, total_weight)
2145
+ cumulative = 0
2146
+ selected_member = candidates[0][0]
2147
+ status_info = candidates[0][1]
2148
+ for (candidate, status), weight in zip(candidates, weights):
2149
+ cumulative += weight
2150
+ if roll <= cumulative:
2151
+ selected_member, status_info = candidate, status
2152
+ break
2153
+ elif strategy == "least_used":
2154
+ candidates.sort(key=lambda x: x[0].get("use_count", 0))
2155
+ selected_member, status_info = candidates[0]
2156
+ else:
2157
+ candidates.sort(key=lambda x: x[0].get("use_count", 0))
2158
+ selected_member, status_info = candidates[0]
2159
+
2160
+ db.increment_member_use(pool_id, selected_member["provider_id"])
2161
+ db.update_member_stats(
2162
+ pool_id,
2163
+ selected_member["provider_id"],
2164
+ success_rate=status_info.get("uptime", selected_member.get("success_rate")),
2165
+ rate_limit_usage=status_info.get("rate_limit", {}).get("usage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2166
+ rate_limit_limit=status_info.get("rate_limit", {}).get("limit", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2167
+ rate_limit_percentage=status_info.get("rate_limit", {}).get("percentage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2168
+ )
2169
+ db.log_pool_rotation(
2170
+ pool_id,
2171
+ selected_member["provider_id"],
2172
+ selected_member["provider_name"],
2173
+ request.get("reason", "manual") if request else "manual"
2174
+ )
2175
+
2176
+ pool_record = db.get_pool(pool_id)
2177
+ payload = build_pool_payload(pool_record, provider_map)
2178
+
2179
+ await broadcast_pool_update("rotated", pool_id, {
2180
+ "provider_id": selected_member["provider_id"],
2181
+ "provider_name": selected_member["provider_name"]
2182
+ })
2183
+
2184
+ return {
2185
+ "message": "Pool rotated successfully",
2186
+ "provider_name": selected_member["provider_name"],
2187
+ "provider_id": selected_member["provider_id"],
2188
+ "total_rotations": pool_record.get("rotation_count", 0),
2189
+ "pool": payload
2190
+ }
2191
+
2192
+
2193
+ @app.get("/api/pools/{pool_id}/history")
2194
+ async def get_pool_history(pool_id: int, limit: int = 20):
2195
+ try:
2196
+ raw_history = db.get_pool_rotation_history(pool_id, limit)
2197
+ except Exception as exc:
2198
+ logger.warning("pool history fetch failed for %s: %s", pool_id, exc)
2199
+ raw_history = []
2200
+ history = transform_rotation_history(raw_history)
2201
+ return {
2202
+ "history": history,
2203
+ "total": len(history)
2204
+ }
2205
+
2206
+
2207
+ @app.get("/api/pools/history")
2208
+ async def get_all_history(limit: int = 50):
2209
+ try:
2210
+ raw_history = db.get_pool_rotation_history(None, limit)
2211
+ except Exception as exc:
2212
+ logger.warning("global pool history fetch failed: %s", exc)
2213
+ raw_history = []
2214
+ history = transform_rotation_history(raw_history)
2215
+ return {
2216
+ "history": history,
2217
+ "total": len(history)
2218
+ }
2219
+
2220
+ @app.get("/api/providers/config")
2221
+ async def get_providers_config():
2222
+ try:
2223
+ config_path = Path(__file__).parent / "providers_config_ultimate.json"
2224
+ with open(config_path, 'r', encoding='utf-8') as f:
2225
+ config = json.load(f)
2226
+ return config
2227
+ except FileNotFoundError:
2228
+ raise HTTPException(status_code=404, detail="Provider config file not found")
2229
+ except json.JSONDecodeError:
2230
+ raise HTTPException(status_code=500, detail="Invalid JSON in provider config")
2231
+
2232
+ @app.get("/api/providers/{provider_id}/health")
2233
+ async def check_provider_health_by_id(provider_id: str):
2234
+ try:
2235
+ config_path = Path(__file__).parent / "providers_config_ultimate.json"
2236
+ with open(config_path, 'r', encoding='utf-8') as f:
2237
+ config = json.load(f)
2238
+
2239
+ provider = config.get('providers', {}).get(provider_id)
2240
+ if not provider:
2241
+ raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
2242
+
2243
+ base_url = provider.get('base_url')
2244
+ if not base_url:
2245
+ return {"status": "unknown", "error": "No base URL configured"}
2246
+
2247
+ import time
2248
+ start_time = time.time()
2249
+
2250
+ async with aiohttp.ClientSession() as session:
2251
+ try:
2252
+ async with session.get(base_url, timeout=aiohttp.ClientTimeout(total=5.0)) as response:
2253
+ response_time = (time.time() - start_time) * 1000
2254
+ status = "online" if response.status in [200, 201, 204, 301, 302, 404] else "offline"
2255
+ return {
2256
+ "status": status,
2257
+ "response_time": round(response_time, 2),
2258
+ "http_status": response.status
2259
+ }
2260
+ except asyncio.TimeoutError:
2261
+ return {"status": "offline", "error": "Timeout after 5s"}
2262
+ except Exception as e:
2263
+ return {"status": "offline", "error": str(e)}
2264
+
2265
+ except Exception as e:
2266
+ raise HTTPException(status_code=500, detail=str(e))
2267
+
2268
+
2269
  if __name__ == "__main__":
2270
+ import os
2271
+
2272
+ # Get port from environment (Hugging Face uses 7860)
2273
  port = int(os.getenv("PORT", 7860))
2274
  host = os.getenv("HOST", "0.0.0.0")
2275
 
 
2276
  print("๐Ÿš€ Crypto Monitor ULTIMATE")
2277
+ print("๐Ÿ“Š Real APIs: CoinGecko, CoinCap, Binance, DeFi Llama, Fear & Greed")
 
2278
  print(f"๐ŸŒ Server: http://{host}:{port}")
2279
  print(f"๐Ÿ“ก API Docs: http://{host}:{port}/docs")
2280
+ print(f"๐ŸŽฏ Environment: {'Hugging Face Spaces' if port == 7860 else 'Local Development'}")
 
2281
 
2282
  uvicorn.run(
2283
  app,