Really-amin commited on
Commit
eb645b9
·
verified ·
1 Parent(s): ff8f8bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +36 -194
app.py CHANGED
@@ -2,6 +2,7 @@
2
  """
3
  Crypto API Monitor ULTIMATE - Real API Integration
4
  Complete professional monitoring system with 100+ real free crypto APIs
 
5
  """
6
 
7
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
@@ -349,7 +350,6 @@ API_PROVIDERS = {
349
  ]
350
  }
351
 
352
- # Fallback data used when upstream APIs یا پایگاه داده در دسترس نیستند
353
  DEFI_FALLBACK = [
354
  {
355
  "name": "Sample Protocol",
@@ -389,7 +389,7 @@ KEY_QUERY_MAP = {
389
  "TronScan": "apikey"
390
  }
391
 
392
- HEALTH_CACHE_TTL = 120 # seconds
393
  provider_health_cache: Dict[str, Dict] = {}
394
 
395
 
@@ -537,10 +537,8 @@ cache = {
537
  "defi": {"data": None, "timestamp": None, "ttl": 300}
538
  }
539
 
540
- # Smart Proxy Mode - Cache which providers need proxy
541
  provider_proxy_cache: Dict[str, Dict] = {}
542
 
543
- # CORS proxy list (from config)
544
  CORS_PROXIES = [
545
  'https://api.allorigins.win/get?url=',
546
  'https://proxy.cors.sh/',
@@ -548,7 +546,6 @@ CORS_PROXIES = [
548
  ]
549
 
550
  def should_use_proxy(provider_name: str) -> bool:
551
- """Check if a provider should use proxy based on past failures"""
552
  if not is_feature_enabled("enableProxyAutoMode"):
553
  return False
554
 
@@ -556,16 +553,13 @@ def should_use_proxy(provider_name: str) -> bool:
556
  if not cached:
557
  return False
558
 
559
- # Check if cache is still valid (5 minutes)
560
  if (datetime.now() - cached.get("timestamp", datetime.now())).total_seconds() > 300:
561
- # Cache expired, remove it
562
  provider_proxy_cache.pop(provider_name, None)
563
  return False
564
 
565
  return cached.get("use_proxy", False)
566
 
567
  def mark_provider_needs_proxy(provider_name: str):
568
- """Mark a provider as needing proxy"""
569
  provider_proxy_cache[provider_name] = {
570
  "use_proxy": True,
571
  "timestamp": datetime.now(),
@@ -574,22 +568,19 @@ def mark_provider_needs_proxy(provider_name: str):
574
  logger.info(f"Provider '{provider_name}' marked for proxy routing")
575
 
576
  def mark_provider_direct_ok(provider_name: str):
577
- """Mark a provider as working with direct connection"""
578
  if provider_name in provider_proxy_cache:
579
  provider_proxy_cache.pop(provider_name)
580
  logger.info(f"Provider '{provider_name}' restored to direct routing")
581
 
582
  async def fetch_with_proxy(session, url: str, proxy_url: str = None):
583
- """Fetch data through a CORS proxy"""
584
  if not proxy_url:
585
- proxy_url = CORS_PROXIES[0] # Default to first proxy
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
- # Some proxies wrap the response
593
  if isinstance(data, dict) and "contents" in data:
594
  return json.loads(data["contents"])
595
  return data
@@ -599,33 +590,20 @@ async def fetch_with_proxy(session, url: str, proxy_url: str = None):
599
  return None
600
 
601
  async def smart_fetch(session, url: str, provider_name: str = None, retries=3):
602
- """
603
- Smart fetch with automatic proxy fallback
604
-
605
- Flow:
606
- 1. If provider is marked for proxy -> use proxy directly
607
- 2. Otherwise, try direct connection
608
- 3. On failure (timeout, CORS, 403, connection error) -> fallback to proxy
609
- 4. Cache the proxy decision for the provider
610
- """
611
- # Check if we should go through proxy directly
612
  if provider_name and should_use_proxy(provider_name):
613
  logger.debug(f"Using proxy for {provider_name} (cached decision)")
614
  return await fetch_with_proxy(session, url)
615
 
616
- # Try direct connection first
617
  for attempt in range(retries):
618
  try:
619
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
620
  if response.status == 200:
621
- # Success! Mark provider as working directly
622
  if provider_name:
623
  mark_provider_direct_ok(provider_name)
624
  return await response.json()
625
- elif response.status == 429: # Rate limit
626
  await asyncio.sleep(2 ** attempt)
627
- elif response.status in [403, 451]: # Forbidden or CORS
628
- # Try proxy fallback
629
  if provider_name:
630
  mark_provider_needs_proxy(provider_name)
631
  logger.info(f"HTTP {response.status} on {url}, trying proxy...")
@@ -633,14 +611,12 @@ async def smart_fetch(session, url: str, provider_name: str = None, retries=3):
633
  else:
634
  return None
635
  except asyncio.TimeoutError:
636
- # Timeout - try proxy on last attempt
637
  if attempt == retries - 1 and provider_name:
638
  mark_provider_needs_proxy(provider_name)
639
  logger.info(f"Timeout on {url}, trying proxy...")
640
  return await fetch_with_proxy(session, url)
641
  await asyncio.sleep(1)
642
  except aiohttp.ClientError as e:
643
- # Network error (connection refused, CORS, etc) - try proxy
644
  if "CORS" in str(e) or "Connection" in str(e) or "SSL" in str(e):
645
  if provider_name:
646
  mark_provider_needs_proxy(provider_name)
@@ -658,25 +634,20 @@ async def smart_fetch(session, url: str, provider_name: str = None, retries=3):
658
 
659
  return None
660
 
661
- # Keep old function for backward compatibility
662
  async def fetch_with_retry(session, url, retries=3):
663
- """Fetch data with retry mechanism (uses smart_fetch internally)"""
664
  return await smart_fetch(session, url, retries=retries)
665
 
666
  def is_cache_valid(cache_entry):
667
- """Check if cache is still valid"""
668
  if cache_entry["data"] is None or cache_entry["timestamp"] is None:
669
  return False
670
  elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
671
  return elapsed < cache_entry["ttl"]
672
 
673
  async def get_market_data():
674
- """Fetch real market data from multiple sources"""
675
  if is_cache_valid(cache["market_data"]):
676
  return cache["market_data"]["data"]
677
 
678
  async with aiohttp.ClientSession() as session:
679
- # Try CoinGecko first
680
  url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1"
681
  data = await fetch_with_retry(session, url)
682
 
@@ -698,7 +669,6 @@ async def get_market_data():
698
  cache["market_data"]["timestamp"] = datetime.now()
699
  return formatted_data
700
 
701
- # Fallback to CoinCap
702
  url = "https://api.coincap.io/v2/assets?limit=20"
703
  data = await fetch_with_retry(session, url)
704
 
@@ -723,9 +693,7 @@ async def get_market_data():
723
  return []
724
 
725
  async def get_global_stats():
726
- """Fetch global crypto market statistics"""
727
  async with aiohttp.ClientSession() as session:
728
- # CoinGecko global data
729
  url = "https://api.coingecko.com/api/v3/global"
730
  data = await fetch_with_retry(session, url)
731
 
@@ -750,7 +718,6 @@ async def get_global_stats():
750
  }
751
 
752
  async def get_trending():
753
- """Fetch trending coins"""
754
  async with aiohttp.ClientSession() as session:
755
  url = "https://api.coingecko.com/api/v3/search/trending"
756
  data = await fetch_with_retry(session, url)
@@ -769,7 +736,6 @@ async def get_trending():
769
  return []
770
 
771
  async def get_sentiment():
772
- """Fetch Fear & Greed Index"""
773
  if is_cache_valid(cache["sentiment"]):
774
  return cache["sentiment"]["data"]
775
 
@@ -791,7 +757,6 @@ async def get_sentiment():
791
  return {"value": 50, "classification": "Neutral", "timestamp": ""}
792
 
793
  async def get_defi_tvl():
794
- """Fetch DeFi Total Value Locked"""
795
  if is_cache_valid(cache["defi"]):
796
  return cache["defi"]["data"]
797
 
@@ -817,7 +782,6 @@ async def get_defi_tvl():
817
  return []
818
 
819
  async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict, force_refresh: bool = False) -> Dict:
820
- """Fetch real health information for a provider"""
821
  name = provider["name"]
822
  cached = provider_health_cache.get(name)
823
  if cached and not force_refresh:
@@ -947,7 +911,6 @@ async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict,
947
 
948
 
949
  async def get_provider_stats(force_refresh: bool = False):
950
- """Generate provider statistics with real health checks"""
951
  providers = assemble_providers()
952
  async with aiohttp.ClientSession() as session:
953
  results = await asyncio.gather(
@@ -1003,12 +966,10 @@ async def health():
1003
 
1004
  @app.get("/api/health")
1005
  async def api_health():
1006
- """Compatibility endpoint mirroring /health"""
1007
  return await health()
1008
 
1009
  @app.get("/api/market")
1010
  async def market():
1011
- """Get real-time market data"""
1012
  data = await get_market_data()
1013
  global_stats = await get_global_stats()
1014
 
@@ -1021,7 +982,6 @@ async def market():
1021
 
1022
  @app.get("/api/trending")
1023
  async def trending():
1024
- """Get trending coins"""
1025
  data = await get_trending()
1026
  return {
1027
  "trending": data,
@@ -1031,7 +991,6 @@ async def trending():
1031
 
1032
  @app.get("/api/sentiment")
1033
  async def sentiment():
1034
- """Get Fear & Greed Index"""
1035
  data = await get_sentiment()
1036
  return {
1037
  "fear_greed_index": data,
@@ -1041,10 +1000,9 @@ async def sentiment():
1041
 
1042
  @app.get("/api/defi")
1043
  async def defi():
1044
- """Get DeFi protocols and TVL"""
1045
  try:
1046
  data = await get_defi_tvl()
1047
- except Exception as exc: # pragma: no cover - defensive
1048
  logger.warning("defi endpoint fallback due to error: %s", exc)
1049
  data = []
1050
 
@@ -1061,20 +1019,17 @@ async def defi():
1061
 
1062
  @app.get("/api/providers")
1063
  async def providers():
1064
- """Get all API providers status"""
1065
  data = await get_provider_stats()
1066
  return data
1067
 
1068
 
1069
  @app.get("/api/providers/custom")
1070
  async def providers_custom():
1071
- """Return custom providers registered through the UI."""
1072
  return _get_custom_providers()
1073
 
1074
 
1075
  @app.post("/api/providers", status_code=201)
1076
  async def create_provider(request: ProviderCreateRequest):
1077
- """Create a custom provider entry."""
1078
  name = request.name.strip()
1079
  if not name:
1080
  raise HTTPException(status_code=400, detail="name is required")
@@ -1106,14 +1061,12 @@ async def create_provider(request: ProviderCreateRequest):
1106
 
1107
  @app.delete("/api/providers/{slug}", status_code=204)
1108
  async def delete_provider(slug: str):
1109
- """Delete a custom provider by slug."""
1110
  if not _remove_custom_provider(slug):
1111
  raise HTTPException(status_code=404, detail="Provider not found")
1112
  return Response(status_code=204)
1113
 
1114
  @app.get("/api/status")
1115
  async def status():
1116
- """Get system status for dashboard"""
1117
  providers = await get_provider_stats()
1118
  online = len([p for p in providers if p.get("status") == "online"])
1119
  offline = len([p for p in providers if p.get("status") == "offline"])
@@ -1153,7 +1106,6 @@ async def system_info():
1153
 
1154
  @app.get("/api/stats")
1155
  async def stats():
1156
- """Get comprehensive statistics"""
1157
  market = await get_market_data()
1158
  global_stats = await get_global_stats()
1159
  providers = await get_provider_stats()
@@ -1184,7 +1136,6 @@ async def stats():
1184
  "timestamp": datetime.now().isoformat()
1185
  }
1186
 
1187
- # HuggingFace endpoints (mock for now)
1188
  @app.get("/api/hf/health")
1189
  async def hf_health():
1190
  return {
@@ -1195,16 +1146,12 @@ async def hf_health():
1195
 
1196
  @app.post("/api/hf/run-sentiment")
1197
  async def hf_run_sentiment(request: SentimentRequest):
1198
- """Run sentiment analysis on crypto text"""
1199
  texts = request.texts
1200
 
1201
- # Mock sentiment analysis
1202
- # In production, this would call HuggingFace API
1203
  results = []
1204
  total_vote = 0
1205
 
1206
  for text in texts:
1207
- # Simple mock sentiment
1208
  text_lower = text.lower()
1209
  positive_words = ["bullish", "strong", "breakout", "pump", "moon", "buy", "up"]
1210
  negative_words = ["bearish", "weak", "crash", "dump", "sell", "down", "drop"]
@@ -1231,15 +1178,12 @@ async def hf_run_sentiment(request: SentimentRequest):
1231
 
1232
  @app.websocket("/ws")
1233
  async def websocket_root(websocket: WebSocket):
1234
- """WebSocket endpoint for compatibility with websocket-client.js"""
1235
  await websocket_endpoint(websocket)
1236
 
1237
  @app.websocket("/ws/live")
1238
  async def websocket_endpoint(websocket: WebSocket):
1239
- """Real-time WebSocket updates"""
1240
  await manager.connect(websocket)
1241
  try:
1242
- # Send welcome message
1243
  await websocket.send_json({
1244
  "type": "welcome",
1245
  "session_id": str(id(websocket)),
@@ -1249,16 +1193,14 @@ async def websocket_endpoint(websocket: WebSocket):
1249
  while True:
1250
  await asyncio.sleep(5)
1251
 
1252
- # Send market update
1253
  market_data = await get_market_data()
1254
  if market_data:
1255
  await websocket.send_json({
1256
  "type": "market_update",
1257
- "data": market_data[:5], # Top 5 coins
1258
  "timestamp": datetime.now().isoformat()
1259
  })
1260
 
1261
- # Send sentiment update every 30 seconds
1262
  if random.random() > 0.8:
1263
  sentiment_data = await get_sentiment()
1264
  await websocket.send_json({
@@ -1278,7 +1220,6 @@ async def websocket_endpoint(websocket: WebSocket):
1278
  async def websocket_endpoint_api(websocket: WebSocket):
1279
  await websocket_endpoint(websocket)
1280
 
1281
- # Serve HTML files
1282
  @app.get("/", response_class=HTMLResponse)
1283
  async def root_html():
1284
  try:
@@ -1349,11 +1290,8 @@ async def pool_management():
1349
 
1350
 
1351
 
1352
- # --- UI helper endpoints for categories, rate limits, logs, alerts, and HuggingFace registry ---
1353
-
1354
  @app.get("/api/categories")
1355
  async def api_categories():
1356
- """Aggregate providers by category for the dashboard UI"""
1357
  providers = await get_provider_stats()
1358
  categories_map: Dict[str, Dict] = {}
1359
  for p in providers:
@@ -1403,7 +1341,6 @@ async def api_categories():
1403
 
1404
  @app.get("/api/rate-limits")
1405
  async def api_rate_limits():
1406
- """Expose simple rate-limit information per provider for the UI cards"""
1407
  providers = await get_provider_stats()
1408
  now = datetime.now()
1409
  items = []
@@ -1455,7 +1392,6 @@ async def api_rate_limits():
1455
 
1456
  @app.get("/api/logs")
1457
  async def api_logs(type: str = "all"):
1458
- """Return recent connection logs from SQLite for the logs tab"""
1459
  rows = db.get_recent_status(hours=24, limit=500)
1460
  logs = []
1461
  for row in rows:
@@ -1481,7 +1417,6 @@ async def api_logs(type: str = "all"):
1481
 
1482
  @app.get("/api/logs/summary")
1483
  async def api_logs_summary(hours: int = 24):
1484
- """Provide aggregated log summary for dashboard widgets."""
1485
  rows = db.get_recent_status(hours=hours, limit=500)
1486
  by_status: Dict[str, int] = defaultdict(int)
1487
  by_provider: Dict[str, int] = defaultdict(int)
@@ -1509,7 +1444,6 @@ async def api_logs_summary(hours: int = 24):
1509
 
1510
  @app.get("/api/alerts")
1511
  async def api_alerts():
1512
- """Expose active/unacknowledged alerts for the alerts tab"""
1513
  try:
1514
  rows = db.get_unacknowledged_alerts()
1515
  except Exception:
@@ -1536,13 +1470,8 @@ HF_CACHE_TS: Optional[datetime] = None
1536
 
1537
 
1538
  async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit: int = 12) -> List[Dict]:
1539
- """
1540
- Fetch a small registry snapshot from Hugging Face Hub.
1541
- If the request fails for any reason, falls back to a small built-in sample.
1542
- """
1543
  global HF_MODELS, HF_DATASETS, HF_CACHE_TS
1544
 
1545
- # Basic in-memory TTL cache (6 hours)
1546
  now = datetime.now()
1547
  if HF_CACHE_TS and (now - HF_CACHE_TS).total_seconds() < 6 * 3600:
1548
  if kind == "models" and HF_MODELS:
@@ -1563,7 +1492,6 @@ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit:
1563
  async with session.get(base_url, params=params, headers=headers, timeout=10) as resp:
1564
  if resp.status == 200:
1565
  raw = await resp.json()
1566
- # HF returns a list of models/datasets
1567
  for entry in raw:
1568
  item = {
1569
  "id": entry.get("id") or entry.get("name"),
@@ -1575,10 +1503,8 @@ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit:
1575
  }
1576
  items.append(item)
1577
  except Exception:
1578
- # ignore and fall back
1579
  items = []
1580
 
1581
- # Fallback sample if nothing was fetched
1582
  if not items:
1583
  if kind == "models":
1584
  items = [
@@ -1611,7 +1537,6 @@ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit:
1611
  },
1612
  ]
1613
 
1614
- # Update cache
1615
  custom_items = _get_custom_hf("models" if kind == "models" else "datasets")
1616
  if custom_items:
1617
  seen_ids = {item.get("id") or item.get("name") for item in items}
@@ -1632,7 +1557,6 @@ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit:
1632
 
1633
  @app.post("/api/hf/refresh")
1634
  async def hf_refresh():
1635
- """Refresh HF registry data used by the UI."""
1636
  models = await _fetch_hf_registry("models")
1637
  datasets = await _fetch_hf_registry("datasets")
1638
  return {"status": "ok", "models": len(models), "datasets": len(datasets)}
@@ -1640,7 +1564,6 @@ async def hf_refresh():
1640
 
1641
  @app.get("/api/hf/registry")
1642
  async def hf_registry(type: str = "models"):
1643
- """Return model/dataset registry for the HF panel."""
1644
  if type == "datasets":
1645
  data = await _fetch_hf_registry("datasets")
1646
  else:
@@ -1650,7 +1573,6 @@ async def hf_registry(type: str = "models"):
1650
 
1651
  @app.get("/api/hf/custom")
1652
  async def hf_custom_registry():
1653
- """Return custom Hugging Face registry entries."""
1654
  return {
1655
  "models": _get_custom_hf("models"),
1656
  "datasets": _get_custom_hf("datasets"),
@@ -1659,7 +1581,6 @@ async def hf_custom_registry():
1659
 
1660
  @app.post("/api/hf/custom", status_code=201)
1661
  async def hf_register_custom(item: HFRegistryItemCreate):
1662
- """Register a custom Hugging Face model or dataset."""
1663
  payload = {
1664
  "id": item.id.strip(),
1665
  "description": item.description.strip() if item.description else "",
@@ -1677,7 +1598,6 @@ async def hf_register_custom(item: HFRegistryItemCreate):
1677
 
1678
  @app.delete("/api/hf/custom/{kind}/{identifier}", status_code=204)
1679
  async def hf_delete_custom(kind: str, identifier: str):
1680
- """Remove a custom HF model or dataset."""
1681
  kind = kind.lower()
1682
  if kind not in {"model", "dataset"}:
1683
  raise HTTPException(status_code=400, detail="kind must be 'model' or 'dataset'")
@@ -1689,7 +1609,6 @@ async def hf_delete_custom(kind: str, identifier: str):
1689
 
1690
  @app.get("/api/hf/search")
1691
  async def hf_search(q: str = "", kind: str = "models"):
1692
- """Search over the HF registry."""
1693
  pool = await _fetch_hf_registry("models" if kind == "models" else "datasets")
1694
  q_lower = (q or "").lower()
1695
  results: List[Dict] = []
@@ -1700,16 +1619,13 @@ async def hf_search(q: str = "", kind: str = "models"):
1700
  return results
1701
 
1702
 
1703
- # Feature Flags Endpoints
1704
  @app.get("/api/feature-flags")
1705
  async def get_feature_flags():
1706
- """Get all feature flags and their status"""
1707
  return feature_flags.get_feature_info()
1708
 
1709
 
1710
  @app.put("/api/feature-flags")
1711
  async def update_feature_flags(request: FeatureFlagsUpdate):
1712
- """Update multiple feature flags"""
1713
  success = feature_flags.update_flags(request.flags)
1714
  if success:
1715
  return {
@@ -1723,7 +1639,6 @@ async def update_feature_flags(request: FeatureFlagsUpdate):
1723
 
1724
  @app.put("/api/feature-flags/{flag_name}")
1725
  async def update_single_feature_flag(flag_name: str, request: FeatureFlagUpdate):
1726
- """Update a single feature flag"""
1727
  success = feature_flags.set_flag(flag_name, request.value)
1728
  if success:
1729
  return {
@@ -1738,7 +1653,6 @@ async def update_single_feature_flag(flag_name: str, request: FeatureFlagUpdate)
1738
 
1739
  @app.post("/api/feature-flags/reset")
1740
  async def reset_feature_flags():
1741
- """Reset all feature flags to default values"""
1742
  success = feature_flags.reset_to_defaults()
1743
  if success:
1744
  return {
@@ -1752,7 +1666,6 @@ async def reset_feature_flags():
1752
 
1753
  @app.get("/api/feature-flags/{flag_name}")
1754
  async def get_single_feature_flag(flag_name: str):
1755
- """Get a single feature flag value"""
1756
  value = feature_flags.get_flag(flag_name)
1757
  return {
1758
  "flag_name": flag_name,
@@ -1763,7 +1676,6 @@ async def get_single_feature_flag(flag_name: str):
1763
 
1764
  @app.get("/api/proxy-status")
1765
  async def get_proxy_status():
1766
- """Get provider proxy routing status"""
1767
  status = []
1768
  for provider_name, cache_data in provider_proxy_cache.items():
1769
  age_seconds = (datetime.now() - cache_data.get("timestamp", datetime.now())).total_seconds()
@@ -1841,12 +1753,16 @@ async def hf_search_legacy(q: str = "", kind: str = "models"):
1841
  return await hf_search(q=q, kind=kind)
1842
 
1843
 
1844
- # Serve static files (JS, CSS, etc.)
1845
- # Serve static files (JS, CSS, etc.)
1846
- if os.path.exists("static"):
1847
  app.mount("/static", StaticFiles(directory="static"), name="static")
 
 
 
 
 
1848
 
1849
- # Serve config.js
1850
  @app.get("/config.js")
1851
  async def config_js():
1852
  try:
@@ -1855,10 +1771,8 @@ async def config_js():
1855
  except:
1856
  return Response(content="// Config not found", media_type="application/javascript")
1857
 
1858
- # API v2 endpoints for enhanced dashboard
1859
  @app.get("/api/v2/status")
1860
  async def v2_status():
1861
- """Enhanced status endpoint"""
1862
  providers = await get_provider_stats()
1863
  return {
1864
  "services": {
@@ -1884,7 +1798,6 @@ async def v2_status():
1884
 
1885
  @app.get("/api/v2/config/apis")
1886
  async def v2_config_apis():
1887
- """Get API configuration"""
1888
  providers = await get_provider_stats()
1889
  apis = {}
1890
  for p in providers:
@@ -1898,7 +1811,6 @@ async def v2_config_apis():
1898
 
1899
  @app.get("/api/v2/schedule/tasks")
1900
  async def v2_schedule_tasks():
1901
- """Get scheduled tasks"""
1902
  providers = await get_provider_stats()
1903
  tasks = {}
1904
  for p in providers:
@@ -1914,7 +1826,6 @@ async def v2_schedule_tasks():
1914
 
1915
  @app.get("/api/v2/schedule/tasks/{api_id}")
1916
  async def v2_schedule_task(api_id: str):
1917
- """Get specific scheduled task"""
1918
  return {
1919
  "api_id": api_id,
1920
  "interval": 300,
@@ -1925,7 +1836,6 @@ async def v2_schedule_task(api_id: str):
1925
 
1926
  @app.put("/api/v2/schedule/tasks/{api_id}")
1927
  async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = True):
1928
- """Update schedule"""
1929
  return {
1930
  "api_id": api_id,
1931
  "interval": interval,
@@ -1935,7 +1845,6 @@ async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = T
1935
 
1936
  @app.post("/api/v2/schedule/tasks/{api_id}/force-update")
1937
  async def v2_force_update(api_id: str):
1938
- """Force update for specific API"""
1939
  return {
1940
  "api_id": api_id,
1941
  "status": "updated",
@@ -1944,7 +1853,6 @@ async def v2_force_update(api_id: str):
1944
 
1945
  @app.post("/api/v2/export/json")
1946
  async def v2_export_json(request: dict):
1947
- """Export data as JSON"""
1948
  market = await get_market_data()
1949
  return {
1950
  "filepath": "export.json",
@@ -1954,7 +1862,6 @@ async def v2_export_json(request: dict):
1954
 
1955
  @app.post("/api/v2/export/csv")
1956
  async def v2_export_csv(request: dict):
1957
- """Export data as CSV"""
1958
  return {
1959
  "filepath": "export.csv",
1960
  "download_url": "/api/v2/export/download/export.csv",
@@ -1963,7 +1870,6 @@ async def v2_export_csv(request: dict):
1963
 
1964
  @app.post("/api/v2/backup")
1965
  async def v2_backup():
1966
- """Create backup"""
1967
  return {
1968
  "backup_file": f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1969
  "timestamp": datetime.now().isoformat()
@@ -1971,8 +1877,6 @@ async def v2_backup():
1971
 
1972
  @app.post("/api/v2/cleanup/cache")
1973
  async def v2_cleanup_cache():
1974
- """Clear cache"""
1975
- # Clear all caches
1976
  for key in cache:
1977
  cache[key]["data"] = None
1978
  cache[key]["timestamp"] = None
@@ -1983,13 +1887,11 @@ async def v2_cleanup_cache():
1983
 
1984
  @app.websocket("/api/v2/ws")
1985
  async def v2_websocket(websocket: WebSocket):
1986
- """Enhanced WebSocket endpoint"""
1987
  await manager.connect(websocket)
1988
  try:
1989
  while True:
1990
  await asyncio.sleep(5)
1991
 
1992
- # Send status update
1993
  await websocket.send_json({
1994
  "type": "status_update",
1995
  "data": {
@@ -2000,7 +1902,6 @@ async def v2_websocket(websocket: WebSocket):
2000
  except WebSocketDisconnect:
2001
  manager.disconnect(websocket)
2002
 
2003
- # Pool Management Helpers and Endpoints
2004
  def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
2005
  members_payload = []
2006
  current_provider = None
@@ -2029,7 +1930,6 @@ def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
2029
  }
2030
  }
2031
 
2032
- # keep database stats in sync
2033
  db.update_member_stats(
2034
  pool["id"],
2035
  provider_id,
@@ -2085,7 +1985,6 @@ async def broadcast_pool_update(action: str, pool_id: int, extra: Optional[Dict]
2085
 
2086
  @app.get("/api/pools")
2087
  async def get_pools():
2088
- """Get all pools"""
2089
  providers = await get_provider_stats()
2090
  provider_map = {provider_slug(p["name"]): p for p in providers}
2091
  pools = db.get_pools()
@@ -2095,7 +1994,6 @@ async def get_pools():
2095
 
2096
  @app.post("/api/pools")
2097
  async def create_pool(pool: PoolCreate):
2098
- """Create a new pool"""
2099
  valid_strategies = {"round_robin", "priority", "weighted", "least_used"}
2100
  if pool.rotation_strategy not in valid_strategies:
2101
  raise HTTPException(status_code=400, detail="Invalid rotation strategy")
@@ -2124,7 +2022,6 @@ async def create_pool(pool: PoolCreate):
2124
 
2125
  @app.get("/api/pools/{pool_id}")
2126
  async def get_pool(pool_id: int):
2127
- """Get specific pool"""
2128
  pool = db.get_pool(pool_id)
2129
  if not pool:
2130
  raise HTTPException(status_code=404, detail="Pool not found")
@@ -2136,7 +2033,6 @@ async def get_pool(pool_id: int):
2136
 
2137
  @app.delete("/api/pools/{pool_id}")
2138
  async def delete_pool(pool_id: int):
2139
- """Delete a pool"""
2140
  pool = db.get_pool(pool_id)
2141
  if not pool:
2142
  raise HTTPException(status_code=404, detail="Pool not found")
@@ -2148,7 +2044,6 @@ async def delete_pool(pool_id: int):
2148
 
2149
  @app.post("/api/pools/{pool_id}/members")
2150
  async def add_pool_member(pool_id: int, member: PoolMemberAdd):
2151
- """Add a member to a pool"""
2152
  pool = db.get_pool(pool_id)
2153
  if not pool:
2154
  raise HTTPException(status_code=404, detail="Pool not found")
@@ -2187,7 +2082,6 @@ async def add_pool_member(pool_id: int, member: PoolMemberAdd):
2187
 
2188
  @app.delete("/api/pools/{pool_id}/members/{provider_id}")
2189
  async def remove_pool_member(pool_id: int, provider_id: str):
2190
- """Remove a member from a pool"""
2191
  pool = db.get_pool(pool_id)
2192
  if not pool:
2193
  raise HTTPException(status_code=404, detail="Pool not found")
@@ -2208,7 +2102,6 @@ async def remove_pool_member(pool_id: int, provider_id: str):
2208
 
2209
  @app.post("/api/pools/{pool_id}/rotate")
2210
  async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
2211
- """Rotate pool to next provider"""
2212
  pool = db.get_pool(pool_id)
2213
  if not pool:
2214
  raise HTTPException(status_code=404, detail="Pool not found")
@@ -2252,7 +2145,7 @@ async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
2252
  elif strategy == "least_used":
2253
  candidates.sort(key=lambda x: x[0].get("use_count", 0))
2254
  selected_member, status_info = candidates[0]
2255
- else: # round_robin or default
2256
  candidates.sort(key=lambda x: x[0].get("use_count", 0))
2257
  selected_member, status_info = candidates[0]
2258
 
@@ -2291,10 +2184,9 @@ async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
2291
 
2292
  @app.get("/api/pools/{pool_id}/history")
2293
  async def get_pool_history(pool_id: int, limit: int = 20):
2294
- """Get rotation history for a pool"""
2295
  try:
2296
  raw_history = db.get_pool_rotation_history(pool_id, limit)
2297
- except Exception as exc: # pragma: no cover - defensive
2298
  logger.warning("pool history fetch failed for %s: %s", pool_id, exc)
2299
  raw_history = []
2300
  history = transform_rotation_history(raw_history)
@@ -2306,10 +2198,9 @@ async def get_pool_history(pool_id: int, limit: int = 20):
2306
 
2307
  @app.get("/api/pools/history")
2308
  async def get_all_history(limit: int = 50):
2309
- """Get all rotation history"""
2310
  try:
2311
  raw_history = db.get_pool_rotation_history(None, limit)
2312
- except Exception as exc: # pragma: no cover - defensive
2313
  logger.warning("global pool history fetch failed: %s", exc)
2314
  raw_history = []
2315
  history = transform_rotation_history(raw_history)
@@ -2320,10 +2211,6 @@ async def get_all_history(limit: int = 50):
2320
 
2321
  @app.get("/api/providers/config")
2322
  async def get_providers_config():
2323
- """
2324
- Return complete provider configuration from providers_config_ultimate.json
2325
- This endpoint is used by the Provider Auto-Discovery Engine
2326
- """
2327
  try:
2328
  config_path = Path(__file__).parent / "providers_config_ultimate.json"
2329
  with open(config_path, 'r', encoding='utf-8') as f:
@@ -2336,12 +2223,7 @@ async def get_providers_config():
2336
 
2337
  @app.get("/api/providers/{provider_id}/health")
2338
  async def check_provider_health_by_id(provider_id: str):
2339
- """
2340
- Check health status of a specific provider
2341
- Returns: { status: 'online'|'offline', response_time: number, error?: string }
2342
- """
2343
  try:
2344
- # Load provider config
2345
  config_path = Path(__file__).parent / "providers_config_ultimate.json"
2346
  with open(config_path, 'r', encoding='utf-8') as f:
2347
  config = json.load(f)
@@ -2350,7 +2232,6 @@ async def check_provider_health_by_id(provider_id: str):
2350
  if not provider:
2351
  raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
2352
 
2353
- # Try to ping the provider's base URL
2354
  base_url = provider.get('base_url')
2355
  if not base_url:
2356
  return {"status": "unknown", "error": "No base URL configured"}
@@ -2361,7 +2242,7 @@ async def check_provider_health_by_id(provider_id: str):
2361
  async with aiohttp.ClientSession() as session:
2362
  try:
2363
  async with session.get(base_url, timeout=aiohttp.ClientTimeout(total=5.0)) as response:
2364
- response_time = (time.time() - start_time) * 1000 # Convert to milliseconds
2365
  status = "online" if response.status in [200, 201, 204, 301, 302, 404] else "offline"
2366
  return {
2367
  "status": status,
@@ -2376,62 +2257,23 @@ async def check_provider_health_by_id(provider_id: str):
2376
  except Exception as e:
2377
  raise HTTPException(status_code=500, detail=str(e))
2378
 
 
2379
  if __name__ == "__main__":
 
 
 
 
 
 
2380
  print("🚀 Crypto Monitor ULTIMATE")
2381
  print("📊 Real APIs: CoinGecko, CoinCap, Binance, DeFi Llama, Fear & Greed")
2382
- print("🌐 http://localhost:8000/dashboard")
2383
- print("📡 API Docs: http://localhost:8000/docs")
2384
- uvicorn.run(app, host="0.0.0.0", port=8000)
2385
-
2386
- # === Compatibility routes without /api prefix for frontend fallbacks ===
2387
-
2388
- @app.get("/providers")
2389
- async def providers_root():
2390
- """Compatibility: mirror /api/providers at /providers"""
2391
- return await providers()
2392
-
2393
- @app.get("/providers/health")
2394
- async def providers_health_root():
2395
- """Compatibility: health-style endpoint for providers"""
2396
- data = await get_provider_stats(force_refresh=True)
2397
- return data
2398
-
2399
- @app.get("/categories")
2400
- async def categories_root():
2401
- """Compatibility: mirror /api/categories at /categories"""
2402
- return await api_categories()
2403
-
2404
- @app.get("/rate-limits")
2405
- async def rate_limits_root():
2406
- """Compatibility: mirror /api/rate-limits at /rate-limits"""
2407
- return await api_rate_limits()
2408
-
2409
- @app.get("/logs")
2410
- async def logs_root(type: str = "all"):
2411
- """Compatibility: mirror /api/logs at /logs"""
2412
- return await api_logs(type=type)
2413
-
2414
- @app.get("/alerts")
2415
- async def alerts_root():
2416
- """Compatibility: mirror /api/alerts at /alerts"""
2417
- return await api_alerts()
2418
-
2419
- @app.get("/hf/health")
2420
- async def hf_health_root():
2421
- """Compatibility: mirror /api/hf/health at /hf/health"""
2422
- return await hf_health()
2423
-
2424
- @app.get("/hf/registry")
2425
- async def hf_registry_root(type: str = "models"):
2426
- """Compatibility: mirror /api/hf/registry at /hf/registry"""
2427
- return await hf_registry(type=type)
2428
-
2429
- @app.get("/hf/search")
2430
- async def hf_search_root(q: str = "", kind: str = "models"):
2431
- """Compatibility: mirror /api/hf/search at /hf/search"""
2432
- return await hf_search(q=q, kind=kind)
2433
-
2434
- @app.post("/hf/run-sentiment")
2435
- async def hf_run_sentiment_root(request: SentimentRequest):
2436
- """Compatibility: mirror /api/hf/run-sentiment at /hf/run-sentiment"""
2437
- return await hf_run_sentiment(request)
 
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
 
350
  ]
351
  }
352
 
 
353
  DEFI_FALLBACK = [
354
  {
355
  "name": "Sample Protocol",
 
389
  "TronScan": "apikey"
390
  }
391
 
392
+ HEALTH_CACHE_TTL = 120
393
  provider_health_cache: Dict[str, Dict] = {}
394
 
395
 
 
537
  "defi": {"data": None, "timestamp": None, "ttl": 300}
538
  }
539
 
 
540
  provider_proxy_cache: Dict[str, Dict] = {}
541
 
 
542
  CORS_PROXIES = [
543
  'https://api.allorigins.win/get?url=',
544
  'https://proxy.cors.sh/',
 
546
  ]
547
 
548
  def should_use_proxy(provider_name: str) -> bool:
 
549
  if not is_feature_enabled("enableProxyAutoMode"):
550
  return False
551
 
 
553
  if not cached:
554
  return False
555
 
 
556
  if (datetime.now() - cached.get("timestamp", datetime.now())).total_seconds() > 300:
 
557
  provider_proxy_cache.pop(provider_name, None)
558
  return False
559
 
560
  return cached.get("use_proxy", False)
561
 
562
  def mark_provider_needs_proxy(provider_name: str):
 
563
  provider_proxy_cache[provider_name] = {
564
  "use_proxy": True,
565
  "timestamp": datetime.now(),
 
568
  logger.info(f"Provider '{provider_name}' marked for proxy routing")
569
 
570
  def mark_provider_direct_ok(provider_name: str):
 
571
  if provider_name in provider_proxy_cache:
572
  provider_proxy_cache.pop(provider_name)
573
  logger.info(f"Provider '{provider_name}' restored to direct routing")
574
 
575
  async def fetch_with_proxy(session, url: str, proxy_url: str = None):
 
576
  if not proxy_url:
577
+ proxy_url = CORS_PROXIES[0]
578
 
579
  try:
580
  proxied_url = f"{proxy_url}{url}"
581
  async with session.get(proxied_url, timeout=aiohttp.ClientTimeout(total=15)) as response:
582
  if response.status == 200:
583
  data = await response.json()
 
584
  if isinstance(data, dict) and "contents" in data:
585
  return json.loads(data["contents"])
586
  return data
 
590
  return None
591
 
592
  async def smart_fetch(session, url: str, provider_name: str = None, retries=3):
 
 
 
 
 
 
 
 
 
 
593
  if provider_name and should_use_proxy(provider_name):
594
  logger.debug(f"Using proxy for {provider_name} (cached decision)")
595
  return await fetch_with_proxy(session, url)
596
 
 
597
  for attempt in range(retries):
598
  try:
599
  async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
600
  if response.status == 200:
 
601
  if provider_name:
602
  mark_provider_direct_ok(provider_name)
603
  return await response.json()
604
+ elif response.status == 429:
605
  await asyncio.sleep(2 ** attempt)
606
+ elif response.status in [403, 451]:
 
607
  if provider_name:
608
  mark_provider_needs_proxy(provider_name)
609
  logger.info(f"HTTP {response.status} on {url}, trying proxy...")
 
611
  else:
612
  return None
613
  except asyncio.TimeoutError:
 
614
  if attempt == retries - 1 and provider_name:
615
  mark_provider_needs_proxy(provider_name)
616
  logger.info(f"Timeout on {url}, trying proxy...")
617
  return await fetch_with_proxy(session, url)
618
  await asyncio.sleep(1)
619
  except aiohttp.ClientError as e:
 
620
  if "CORS" in str(e) or "Connection" in str(e) or "SSL" in str(e):
621
  if provider_name:
622
  mark_provider_needs_proxy(provider_name)
 
634
 
635
  return None
636
 
 
637
  async def fetch_with_retry(session, url, retries=3):
 
638
  return await smart_fetch(session, url, retries=retries)
639
 
640
  def is_cache_valid(cache_entry):
 
641
  if cache_entry["data"] is None or cache_entry["timestamp"] is None:
642
  return False
643
  elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
644
  return elapsed < cache_entry["ttl"]
645
 
646
  async def get_market_data():
 
647
  if is_cache_valid(cache["market_data"]):
648
  return cache["market_data"]["data"]
649
 
650
  async with aiohttp.ClientSession() as session:
 
651
  url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1"
652
  data = await fetch_with_retry(session, url)
653
 
 
669
  cache["market_data"]["timestamp"] = datetime.now()
670
  return formatted_data
671
 
 
672
  url = "https://api.coincap.io/v2/assets?limit=20"
673
  data = await fetch_with_retry(session, url)
674
 
 
693
  return []
694
 
695
  async def get_global_stats():
 
696
  async with aiohttp.ClientSession() as session:
 
697
  url = "https://api.coingecko.com/api/v3/global"
698
  data = await fetch_with_retry(session, url)
699
 
 
718
  }
719
 
720
  async def get_trending():
 
721
  async with aiohttp.ClientSession() as session:
722
  url = "https://api.coingecko.com/api/v3/search/trending"
723
  data = await fetch_with_retry(session, url)
 
736
  return []
737
 
738
  async def get_sentiment():
 
739
  if is_cache_valid(cache["sentiment"]):
740
  return cache["sentiment"]["data"]
741
 
 
757
  return {"value": 50, "classification": "Neutral", "timestamp": ""}
758
 
759
  async def get_defi_tvl():
 
760
  if is_cache_valid(cache["defi"]):
761
  return cache["defi"]["data"]
762
 
 
782
  return []
783
 
784
  async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict, force_refresh: bool = False) -> Dict:
 
785
  name = provider["name"]
786
  cached = provider_health_cache.get(name)
787
  if cached and not force_refresh:
 
911
 
912
 
913
  async def get_provider_stats(force_refresh: bool = False):
 
914
  providers = assemble_providers()
915
  async with aiohttp.ClientSession() as session:
916
  results = await asyncio.gather(
 
966
 
967
  @app.get("/api/health")
968
  async def api_health():
 
969
  return await health()
970
 
971
  @app.get("/api/market")
972
  async def market():
 
973
  data = await get_market_data()
974
  global_stats = await get_global_stats()
975
 
 
982
 
983
  @app.get("/api/trending")
984
  async def trending():
 
985
  data = await get_trending()
986
  return {
987
  "trending": data,
 
991
 
992
  @app.get("/api/sentiment")
993
  async def sentiment():
 
994
  data = await get_sentiment()
995
  return {
996
  "fear_greed_index": data,
 
1000
 
1001
  @app.get("/api/defi")
1002
  async def defi():
 
1003
  try:
1004
  data = await get_defi_tvl()
1005
+ except Exception as exc:
1006
  logger.warning("defi endpoint fallback due to error: %s", exc)
1007
  data = []
1008
 
 
1019
 
1020
  @app.get("/api/providers")
1021
  async def providers():
 
1022
  data = await get_provider_stats()
1023
  return data
1024
 
1025
 
1026
  @app.get("/api/providers/custom")
1027
  async def providers_custom():
 
1028
  return _get_custom_providers()
1029
 
1030
 
1031
  @app.post("/api/providers", status_code=201)
1032
  async def create_provider(request: ProviderCreateRequest):
 
1033
  name = request.name.strip()
1034
  if not name:
1035
  raise HTTPException(status_code=400, detail="name is required")
 
1061
 
1062
  @app.delete("/api/providers/{slug}", status_code=204)
1063
  async def delete_provider(slug: str):
 
1064
  if not _remove_custom_provider(slug):
1065
  raise HTTPException(status_code=404, detail="Provider not found")
1066
  return Response(status_code=204)
1067
 
1068
  @app.get("/api/status")
1069
  async def status():
 
1070
  providers = await get_provider_stats()
1071
  online = len([p for p in providers if p.get("status") == "online"])
1072
  offline = len([p for p in providers if p.get("status") == "offline"])
 
1106
 
1107
  @app.get("/api/stats")
1108
  async def stats():
 
1109
  market = await get_market_data()
1110
  global_stats = await get_global_stats()
1111
  providers = await get_provider_stats()
 
1136
  "timestamp": datetime.now().isoformat()
1137
  }
1138
 
 
1139
  @app.get("/api/hf/health")
1140
  async def hf_health():
1141
  return {
 
1146
 
1147
  @app.post("/api/hf/run-sentiment")
1148
  async def hf_run_sentiment(request: SentimentRequest):
 
1149
  texts = request.texts
1150
 
 
 
1151
  results = []
1152
  total_vote = 0
1153
 
1154
  for text in texts:
 
1155
  text_lower = text.lower()
1156
  positive_words = ["bullish", "strong", "breakout", "pump", "moon", "buy", "up"]
1157
  negative_words = ["bearish", "weak", "crash", "dump", "sell", "down", "drop"]
 
1178
 
1179
  @app.websocket("/ws")
1180
  async def websocket_root(websocket: WebSocket):
 
1181
  await websocket_endpoint(websocket)
1182
 
1183
  @app.websocket("/ws/live")
1184
  async def websocket_endpoint(websocket: WebSocket):
 
1185
  await manager.connect(websocket)
1186
  try:
 
1187
  await websocket.send_json({
1188
  "type": "welcome",
1189
  "session_id": str(id(websocket)),
 
1193
  while True:
1194
  await asyncio.sleep(5)
1195
 
 
1196
  market_data = await get_market_data()
1197
  if market_data:
1198
  await websocket.send_json({
1199
  "type": "market_update",
1200
+ "data": market_data[:5],
1201
  "timestamp": datetime.now().isoformat()
1202
  })
1203
 
 
1204
  if random.random() > 0.8:
1205
  sentiment_data = await get_sentiment()
1206
  await websocket.send_json({
 
1220
  async def websocket_endpoint_api(websocket: WebSocket):
1221
  await websocket_endpoint(websocket)
1222
 
 
1223
  @app.get("/", response_class=HTMLResponse)
1224
  async def root_html():
1225
  try:
 
1290
 
1291
 
1292
 
 
 
1293
  @app.get("/api/categories")
1294
  async def api_categories():
 
1295
  providers = await get_provider_stats()
1296
  categories_map: Dict[str, Dict] = {}
1297
  for p in providers:
 
1341
 
1342
  @app.get("/api/rate-limits")
1343
  async def api_rate_limits():
 
1344
  providers = await get_provider_stats()
1345
  now = datetime.now()
1346
  items = []
 
1392
 
1393
  @app.get("/api/logs")
1394
  async def api_logs(type: str = "all"):
 
1395
  rows = db.get_recent_status(hours=24, limit=500)
1396
  logs = []
1397
  for row in rows:
 
1417
 
1418
  @app.get("/api/logs/summary")
1419
  async def api_logs_summary(hours: int = 24):
 
1420
  rows = db.get_recent_status(hours=hours, limit=500)
1421
  by_status: Dict[str, int] = defaultdict(int)
1422
  by_provider: Dict[str, int] = defaultdict(int)
 
1444
 
1445
  @app.get("/api/alerts")
1446
  async def api_alerts():
 
1447
  try:
1448
  rows = db.get_unacknowledged_alerts()
1449
  except Exception:
 
1470
 
1471
 
1472
  async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit: int = 12) -> List[Dict]:
 
 
 
 
1473
  global HF_MODELS, HF_DATASETS, HF_CACHE_TS
1474
 
 
1475
  now = datetime.now()
1476
  if HF_CACHE_TS and (now - HF_CACHE_TS).total_seconds() < 6 * 3600:
1477
  if kind == "models" and HF_MODELS:
 
1492
  async with session.get(base_url, params=params, headers=headers, timeout=10) as resp:
1493
  if resp.status == 200:
1494
  raw = await resp.json()
 
1495
  for entry in raw:
1496
  item = {
1497
  "id": entry.get("id") or entry.get("name"),
 
1503
  }
1504
  items.append(item)
1505
  except Exception:
 
1506
  items = []
1507
 
 
1508
  if not items:
1509
  if kind == "models":
1510
  items = [
 
1537
  },
1538
  ]
1539
 
 
1540
  custom_items = _get_custom_hf("models" if kind == "models" else "datasets")
1541
  if custom_items:
1542
  seen_ids = {item.get("id") or item.get("name") for item in items}
 
1557
 
1558
  @app.post("/api/hf/refresh")
1559
  async def hf_refresh():
 
1560
  models = await _fetch_hf_registry("models")
1561
  datasets = await _fetch_hf_registry("datasets")
1562
  return {"status": "ok", "models": len(models), "datasets": len(datasets)}
 
1564
 
1565
  @app.get("/api/hf/registry")
1566
  async def hf_registry(type: str = "models"):
 
1567
  if type == "datasets":
1568
  data = await _fetch_hf_registry("datasets")
1569
  else:
 
1573
 
1574
  @app.get("/api/hf/custom")
1575
  async def hf_custom_registry():
 
1576
  return {
1577
  "models": _get_custom_hf("models"),
1578
  "datasets": _get_custom_hf("datasets"),
 
1581
 
1582
  @app.post("/api/hf/custom", status_code=201)
1583
  async def hf_register_custom(item: HFRegistryItemCreate):
 
1584
  payload = {
1585
  "id": item.id.strip(),
1586
  "description": item.description.strip() if item.description else "",
 
1598
 
1599
  @app.delete("/api/hf/custom/{kind}/{identifier}", status_code=204)
1600
  async def hf_delete_custom(kind: str, identifier: str):
 
1601
  kind = kind.lower()
1602
  if kind not in {"model", "dataset"}:
1603
  raise HTTPException(status_code=400, detail="kind must be 'model' or 'dataset'")
 
1609
 
1610
  @app.get("/api/hf/search")
1611
  async def hf_search(q: str = "", kind: str = "models"):
 
1612
  pool = await _fetch_hf_registry("models" if kind == "models" else "datasets")
1613
  q_lower = (q or "").lower()
1614
  results: List[Dict] = []
 
1619
  return results
1620
 
1621
 
 
1622
  @app.get("/api/feature-flags")
1623
  async def get_feature_flags():
 
1624
  return feature_flags.get_feature_info()
1625
 
1626
 
1627
  @app.put("/api/feature-flags")
1628
  async def update_feature_flags(request: FeatureFlagsUpdate):
 
1629
  success = feature_flags.update_flags(request.flags)
1630
  if success:
1631
  return {
 
1639
 
1640
  @app.put("/api/feature-flags/{flag_name}")
1641
  async def update_single_feature_flag(flag_name: str, request: FeatureFlagUpdate):
 
1642
  success = feature_flags.set_flag(flag_name, request.value)
1643
  if success:
1644
  return {
 
1653
 
1654
  @app.post("/api/feature-flags/reset")
1655
  async def reset_feature_flags():
 
1656
  success = feature_flags.reset_to_defaults()
1657
  if success:
1658
  return {
 
1666
 
1667
  @app.get("/api/feature-flags/{flag_name}")
1668
  async def get_single_feature_flag(flag_name: str):
 
1669
  value = feature_flags.get_flag(flag_name)
1670
  return {
1671
  "flag_name": flag_name,
 
1676
 
1677
  @app.get("/api/proxy-status")
1678
  async def get_proxy_status():
 
1679
  status = []
1680
  for provider_name, cache_data in provider_proxy_cache.items():
1681
  age_seconds = (datetime.now() - cache_data.get("timestamp", datetime.now())).total_seconds()
 
1753
  return await hf_search(q=q, kind=kind)
1754
 
1755
 
1756
+ # Serve static files
1757
+ static_dir = Path("static")
1758
+ if static_dir.exists() and static_dir.is_dir():
1759
  app.mount("/static", StaticFiles(directory="static"), name="static")
1760
+ else:
1761
+ static_dir.mkdir(exist_ok=True)
1762
+ (static_dir / "css").mkdir(exist_ok=True)
1763
+ (static_dir / "js").mkdir(exist_ok=True)
1764
+ print("⚠️ Warning: Static files directory created but empty")
1765
 
 
1766
  @app.get("/config.js")
1767
  async def config_js():
1768
  try:
 
1771
  except:
1772
  return Response(content="// Config not found", media_type="application/javascript")
1773
 
 
1774
  @app.get("/api/v2/status")
1775
  async def v2_status():
 
1776
  providers = await get_provider_stats()
1777
  return {
1778
  "services": {
 
1798
 
1799
  @app.get("/api/v2/config/apis")
1800
  async def v2_config_apis():
 
1801
  providers = await get_provider_stats()
1802
  apis = {}
1803
  for p in providers:
 
1811
 
1812
  @app.get("/api/v2/schedule/tasks")
1813
  async def v2_schedule_tasks():
 
1814
  providers = await get_provider_stats()
1815
  tasks = {}
1816
  for p in providers:
 
1826
 
1827
  @app.get("/api/v2/schedule/tasks/{api_id}")
1828
  async def v2_schedule_task(api_id: str):
 
1829
  return {
1830
  "api_id": api_id,
1831
  "interval": 300,
 
1836
 
1837
  @app.put("/api/v2/schedule/tasks/{api_id}")
1838
  async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = True):
 
1839
  return {
1840
  "api_id": api_id,
1841
  "interval": interval,
 
1845
 
1846
  @app.post("/api/v2/schedule/tasks/{api_id}/force-update")
1847
  async def v2_force_update(api_id: str):
 
1848
  return {
1849
  "api_id": api_id,
1850
  "status": "updated",
 
1853
 
1854
  @app.post("/api/v2/export/json")
1855
  async def v2_export_json(request: dict):
 
1856
  market = await get_market_data()
1857
  return {
1858
  "filepath": "export.json",
 
1862
 
1863
  @app.post("/api/v2/export/csv")
1864
  async def v2_export_csv(request: dict):
 
1865
  return {
1866
  "filepath": "export.csv",
1867
  "download_url": "/api/v2/export/download/export.csv",
 
1870
 
1871
  @app.post("/api/v2/backup")
1872
  async def v2_backup():
 
1873
  return {
1874
  "backup_file": f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1875
  "timestamp": datetime.now().isoformat()
 
1877
 
1878
  @app.post("/api/v2/cleanup/cache")
1879
  async def v2_cleanup_cache():
 
 
1880
  for key in cache:
1881
  cache[key]["data"] = None
1882
  cache[key]["timestamp"] = None
 
1887
 
1888
  @app.websocket("/api/v2/ws")
1889
  async def v2_websocket(websocket: WebSocket):
 
1890
  await manager.connect(websocket)
1891
  try:
1892
  while True:
1893
  await asyncio.sleep(5)
1894
 
 
1895
  await websocket.send_json({
1896
  "type": "status_update",
1897
  "data": {
 
1902
  except WebSocketDisconnect:
1903
  manager.disconnect(websocket)
1904
 
 
1905
  def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
1906
  members_payload = []
1907
  current_provider = None
 
1930
  }
1931
  }
1932
 
 
1933
  db.update_member_stats(
1934
  pool["id"],
1935
  provider_id,
 
1985
 
1986
  @app.get("/api/pools")
1987
  async def get_pools():
 
1988
  providers = await get_provider_stats()
1989
  provider_map = {provider_slug(p["name"]): p for p in providers}
1990
  pools = db.get_pools()
 
1994
 
1995
  @app.post("/api/pools")
1996
  async def create_pool(pool: PoolCreate):
 
1997
  valid_strategies = {"round_robin", "priority", "weighted", "least_used"}
1998
  if pool.rotation_strategy not in valid_strategies:
1999
  raise HTTPException(status_code=400, detail="Invalid rotation strategy")
 
2022
 
2023
  @app.get("/api/pools/{pool_id}")
2024
  async def get_pool(pool_id: int):
 
2025
  pool = db.get_pool(pool_id)
2026
  if not pool:
2027
  raise HTTPException(status_code=404, detail="Pool not found")
 
2033
 
2034
  @app.delete("/api/pools/{pool_id}")
2035
  async def delete_pool(pool_id: int):
 
2036
  pool = db.get_pool(pool_id)
2037
  if not pool:
2038
  raise HTTPException(status_code=404, detail="Pool not found")
 
2044
 
2045
  @app.post("/api/pools/{pool_id}/members")
2046
  async def add_pool_member(pool_id: int, member: PoolMemberAdd):
 
2047
  pool = db.get_pool(pool_id)
2048
  if not pool:
2049
  raise HTTPException(status_code=404, detail="Pool not found")
 
2082
 
2083
  @app.delete("/api/pools/{pool_id}/members/{provider_id}")
2084
  async def remove_pool_member(pool_id: int, provider_id: str):
 
2085
  pool = db.get_pool(pool_id)
2086
  if not pool:
2087
  raise HTTPException(status_code=404, detail="Pool not found")
 
2102
 
2103
  @app.post("/api/pools/{pool_id}/rotate")
2104
  async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
 
2105
  pool = db.get_pool(pool_id)
2106
  if not pool:
2107
  raise HTTPException(status_code=404, detail="Pool not found")
 
2145
  elif strategy == "least_used":
2146
  candidates.sort(key=lambda x: x[0].get("use_count", 0))
2147
  selected_member, status_info = candidates[0]
2148
+ else:
2149
  candidates.sort(key=lambda x: x[0].get("use_count", 0))
2150
  selected_member, status_info = candidates[0]
2151
 
 
2184
 
2185
  @app.get("/api/pools/{pool_id}/history")
2186
  async def get_pool_history(pool_id: int, limit: int = 20):
 
2187
  try:
2188
  raw_history = db.get_pool_rotation_history(pool_id, limit)
2189
+ except Exception as exc:
2190
  logger.warning("pool history fetch failed for %s: %s", pool_id, exc)
2191
  raw_history = []
2192
  history = transform_rotation_history(raw_history)
 
2198
 
2199
  @app.get("/api/pools/history")
2200
  async def get_all_history(limit: int = 50):
 
2201
  try:
2202
  raw_history = db.get_pool_rotation_history(None, limit)
2203
+ except Exception as exc:
2204
  logger.warning("global pool history fetch failed: %s", exc)
2205
  raw_history = []
2206
  history = transform_rotation_history(raw_history)
 
2211
 
2212
  @app.get("/api/providers/config")
2213
  async def get_providers_config():
 
 
 
 
2214
  try:
2215
  config_path = Path(__file__).parent / "providers_config_ultimate.json"
2216
  with open(config_path, 'r', encoding='utf-8') as f:
 
2223
 
2224
  @app.get("/api/providers/{provider_id}/health")
2225
  async def check_provider_health_by_id(provider_id: str):
 
 
 
 
2226
  try:
 
2227
  config_path = Path(__file__).parent / "providers_config_ultimate.json"
2228
  with open(config_path, 'r', encoding='utf-8') as f:
2229
  config = json.load(f)
 
2232
  if not provider:
2233
  raise HTTPException(status_code=404, detail=f"Provider '{provider_id}' not found")
2234
 
 
2235
  base_url = provider.get('base_url')
2236
  if not base_url:
2237
  return {"status": "unknown", "error": "No base URL configured"}
 
2242
  async with aiohttp.ClientSession() as session:
2243
  try:
2244
  async with session.get(base_url, timeout=aiohttp.ClientTimeout(total=5.0)) as response:
2245
+ response_time = (time.time() - start_time) * 1000
2246
  status = "online" if response.status in [200, 201, 204, 301, 302, 404] else "offline"
2247
  return {
2248
  "status": status,
 
2257
  except Exception as e:
2258
  raise HTTPException(status_code=500, detail=str(e))
2259
 
2260
+
2261
  if __name__ == "__main__":
2262
+ import os
2263
+
2264
+ # Get port from environment (Hugging Face uses 7860)
2265
+ port = int(os.getenv("PORT", 7860))
2266
+ host = os.getenv("HOST", "0.0.0.0")
2267
+
2268
  print("🚀 Crypto Monitor ULTIMATE")
2269
  print("📊 Real APIs: CoinGecko, CoinCap, Binance, DeFi Llama, Fear & Greed")
2270
+ print(f"🌐 Server: http://{host}:{port}")
2271
+ print(f"📡 API Docs: http://{host}:{port}/docs")
2272
+ print(f"🎯 Environment: {'Hugging Face Spaces' if port == 7860 else 'Local Development'}")
2273
+
2274
+ uvicorn.run(
2275
+ app,
2276
+ host=host,
2277
+ port=port,
2278
+ log_level="info"
2279
+ )