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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -1940
app.py CHANGED
@@ -1,34 +1,102 @@
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,7 +122,6 @@ class ProviderCreateRequest(BaseModel):
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,18 +136,21 @@ class FeatureFlagUpdate(BaseModel):
69
  class FeatureFlagsUpdate(BaseModel):
70
  flags: Dict[str, bool]
71
 
72
- logger = logging.getLogger("crypto_monitor")
73
-
74
-
75
- app = FastAPI(title="Crypto Monitor Ultimate", version="3.0.0")
 
 
76
 
 
77
 
 
78
  def _split_env_list(value: Optional[str]) -> List[str]:
79
  if not value:
80
  return []
81
  return [item.strip() for item in value.split(",") if item.strip()]
82
 
83
-
84
  allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "")
85
  allowed_origin_regex_env = os.getenv("ALLOWED_ORIGIN_REGEX")
86
  allowed_origins = _split_env_list(allowed_origins_env)
@@ -105,7 +175,9 @@ if not trusted_hosts:
105
  trusted_hosts = ["*"]
106
  app.add_middleware(TrustedHostMiddleware, allowed_hosts=trusted_hosts)
107
 
 
108
 
 
109
  CUSTOM_REGISTRY_PATH = Path("data/custom_registry.json")
110
  _registry_lock = Lock()
111
  _custom_registry: Dict[str, List[Dict]] = {
@@ -114,14 +186,9 @@ _custom_registry: Dict[str, List[Dict]] = {
114
  "hf_datasets": []
115
  }
116
 
117
-
118
  def _load_custom_registry() -> Dict[str, List[Dict]]:
119
  if not CUSTOM_REGISTRY_PATH.exists():
120
- return {
121
- "providers": [],
122
- "hf_models": [],
123
- "hf_datasets": []
124
- }
125
  try:
126
  with CUSTOM_REGISTRY_PATH.open("r", encoding="utf-8") as f:
127
  data = json.load(f)
@@ -130,26 +197,20 @@ def _load_custom_registry() -> Dict[str, List[Dict]]:
130
  "hf_models": data.get("hf_models", []),
131
  "hf_datasets": data.get("hf_datasets", []),
132
  }
133
- except Exception:
134
- return {
135
- "providers": [],
136
- "hf_models": [],
137
- "hf_datasets": []
138
- }
139
-
140
 
141
  def _save_custom_registry() -> None:
142
  CUSTOM_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
143
  with CUSTOM_REGISTRY_PATH.open("w", encoding="utf-8") as f:
144
  json.dump(_custom_registry, f, ensure_ascii=False, indent=2)
145
 
146
-
147
  def _refresh_custom_registry() -> None:
148
  global _custom_registry
149
  with _registry_lock:
150
  _custom_registry = _load_custom_registry()
151
 
152
-
153
  _refresh_custom_registry()
154
 
155
  # WebSocket Manager
@@ -162,7 +223,8 @@ class ConnectionManager:
162
  self.active_connections.append(websocket)
163
 
164
  def disconnect(self, websocket: WebSocket):
165
- self.active_connections.remove(websocket)
 
166
 
167
  async def broadcast(self, message: dict):
168
  for connection in self.active_connections:
@@ -173,9 +235,11 @@ class ConnectionManager:
173
 
174
  manager = ConnectionManager()
175
 
 
176
  db = Database("data/crypto_monitor.db")
 
177
 
178
- # API Provider Configuration - Real Free APIs
179
  API_PROVIDERS = {
180
  "market_data": [
181
  {
@@ -187,118 +251,31 @@ API_PROVIDERS = {
187
  "global": "/global",
188
  "trending": "/search/trending"
189
  },
190
- "auth": None,
191
  "rate_limit": "50/min",
192
  "status": "active"
193
  },
194
  {
195
  "name": "CoinCap",
196
  "base_url": "https://api.coincap.io/v2",
197
- "endpoints": {
198
- "assets": "/assets",
199
- "rates": "/rates"
200
- },
201
- "auth": None,
202
  "rate_limit": "200/min",
203
  "status": "active"
204
- },
205
- {
206
- "name": "CoinStats",
207
- "base_url": "https://api.coinstats.app",
208
- "endpoints": {
209
- "coins": "/public/v1/coins",
210
- "charts": "/public/v1/charts"
211
- },
212
- "auth": None,
213
- "rate_limit": "unlimited",
214
- "status": "active"
215
- },
216
- {
217
- "name": "Cryptorank",
218
- "base_url": "https://api.cryptorank.io/v1",
219
- "endpoints": {
220
- "currencies": "/currencies"
221
- },
222
- "auth": None,
223
- "rate_limit": "100/min",
224
- "status": "active"
225
  }
226
  ],
227
  "exchanges": [
228
  {
229
  "name": "Binance",
230
  "base_url": "https://api.binance.com/api/v3",
231
- "endpoints": {
232
- "ticker": "/ticker/24hr",
233
- "price": "/ticker/price"
234
- },
235
- "auth": None,
236
  "rate_limit": "1200/min",
237
  "status": "active"
238
- },
239
- {
240
- "name": "Coinbase",
241
- "base_url": "https://api.coinbase.com/v2",
242
- "endpoints": {
243
- "prices": "/prices",
244
- "exchange_rates": "/exchange-rates"
245
- },
246
- "auth": None,
247
- "rate_limit": "10000/hour",
248
- "status": "active"
249
- },
250
- {
251
- "name": "Kraken",
252
- "base_url": "https://api.kraken.com/0/public",
253
- "endpoints": {
254
- "ticker": "/Ticker",
255
- "trades": "/Trades"
256
- },
257
- "auth": None,
258
- "rate_limit": "1/sec",
259
- "status": "active"
260
- }
261
- ],
262
- "news": [
263
- {
264
- "name": "CoinStats News",
265
- "base_url": "https://api.coinstats.app",
266
- "endpoints": {
267
- "feed": "/public/v1/news"
268
- },
269
- "auth": None,
270
- "rate_limit": "unlimited",
271
- "status": "active"
272
- },
273
- {
274
- "name": "CoinDesk RSS",
275
- "base_url": "https://www.coindesk.com",
276
- "endpoints": {
277
- "rss": "/arc/outboundfeeds/rss/?outputType=xml"
278
- },
279
- "auth": None,
280
- "rate_limit": "unlimited",
281
- "status": "active"
282
- },
283
- {
284
- "name": "Cointelegraph RSS",
285
- "base_url": "https://cointelegraph.com",
286
- "endpoints": {
287
- "rss": "/rss"
288
- },
289
- "auth": None,
290
- "rate_limit": "unlimited",
291
- "status": "active"
292
  }
293
  ],
294
  "sentiment": [
295
  {
296
  "name": "Alternative.me Fear & Greed",
297
  "base_url": "https://api.alternative.me",
298
- "endpoints": {
299
- "fng": "/fng/?limit=1&format=json"
300
- },
301
- "auth": None,
302
  "rate_limit": "unlimited",
303
  "status": "active"
304
  }
@@ -307,348 +284,72 @@ API_PROVIDERS = {
307
  {
308
  "name": "DeFi Llama",
309
  "base_url": "https://api.llama.fi",
310
- "endpoints": {
311
- "protocols": "/protocols",
312
- "tvl": "/tvl"
313
- },
314
- "auth": None,
315
- "rate_limit": "unlimited",
316
- "status": "active"
317
- },
318
- {
319
- "name": "1inch",
320
- "base_url": "https://api.1inch.io/v5.0/1",
321
- "endpoints": {
322
- "quote": "/quote"
323
- },
324
- "auth": None,
325
- "rate_limit": "unlimited",
326
- "status": "active"
327
- }
328
- ],
329
- "blockchain": [
330
- {
331
- "name": "Blockscout Ethereum",
332
- "base_url": "https://eth.blockscout.com/api",
333
- "endpoints": {
334
- "balance": "/v2/addresses"
335
- },
336
- "auth": None,
337
  "rate_limit": "unlimited",
338
  "status": "active"
339
- },
340
- {
341
- "name": "Ethplorer",
342
- "base_url": "https://api.ethplorer.io",
343
- "endpoints": {
344
- "address": "/getAddressInfo"
345
- },
346
- "auth": {"type": "query", "key": "freekey"},
347
- "rate_limit": "limited",
348
- "status": "active"
349
  }
350
  ]
351
  }
352
 
353
- DEFI_FALLBACK = [
354
- {
355
- "name": "Sample Protocol",
356
- "tvl": 0.0,
357
- "change_24h": 0.0,
358
- "chain": "N/A",
359
- }
360
- ]
361
-
362
- # Health check configuration
363
- HEALTH_TESTS = {
364
- "CoinGecko": {"path": "/ping"},
365
- "CoinCap": {"path": "/assets/bitcoin", "params": {"limit": 1}},
366
- "CoinStats": {"path": "/public/v1/coins", "params": {"skip": 0, "limit": 1}},
367
- "CoinStats News": {"path": "/public/v1/news", "params": {"skip": 0, "limit": 1}},
368
- "Cryptorank": {"path": "/currencies"},
369
- "Binance": {"path": "/ping"},
370
- "Coinbase": {"path": "/exchange-rates"},
371
- "Kraken": {"path": "/SystemStatus"},
372
- "Alternative.me Fear & Greed": {"path": "/fng/", "params": {"limit": 1, "format": "json"}},
373
- "DeFi Llama": {"path": "/protocols"},
374
- "1inch": {"path": "/tokens"},
375
- "Blockscout Ethereum": {"path": "/v2/stats"},
376
- "Ethplorer": {"path": "/getTop", "params": {"apikey": "freekey"}},
377
- "CoinDesk RSS": {"path": "/arc/outboundfeeds/rss/?outputType=xml"},
378
- "Cointelegraph RSS": {"path": "/rss"}
379
- }
380
-
381
- KEY_HEADER_MAP = {
382
- "CoinMarketCap": ("X-CMC_PRO_API_KEY", "plain"),
383
- "CryptoCompare": ("Authorization", "apikey")
384
- }
385
-
386
- KEY_QUERY_MAP = {
387
- "Etherscan": "apikey",
388
- "BscScan": "apikey",
389
- "TronScan": "apikey"
390
- }
391
-
392
- HEALTH_CACHE_TTL = 120
393
- provider_health_cache: Dict[str, Dict] = {}
394
-
395
-
396
- def provider_slug(name: str) -> str:
397
- return name.lower().replace(" ", "_")
398
-
399
-
400
- def _get_custom_providers() -> List[Dict]:
401
- with _registry_lock:
402
- return [dict(provider) for provider in _custom_registry.get("providers", [])]
403
-
404
-
405
- def _add_custom_provider(payload: Dict) -> Dict:
406
- slug = provider_slug(payload["name"])
407
- with _registry_lock:
408
- existing = _custom_registry.setdefault("providers", [])
409
- if any(provider_slug(item.get("name", "")) == slug for item in existing):
410
- raise ValueError("Provider already exists")
411
- existing.append(payload)
412
- _save_custom_registry()
413
- return payload
414
-
415
-
416
- def _remove_custom_provider(slug: str) -> bool:
417
- removed = False
418
- with _registry_lock:
419
- providers = _custom_registry.setdefault("providers", [])
420
- new_list = []
421
- for item in providers:
422
- if provider_slug(item.get("name", "")) == slug:
423
- removed = True
424
- continue
425
- new_list.append(item)
426
- if removed:
427
- _custom_registry["providers"] = new_list
428
- _save_custom_registry()
429
- return removed
430
-
431
-
432
- def _get_custom_hf(kind: Literal["models", "datasets"]) -> List[Dict]:
433
- key = "hf_models" if kind == "models" else "hf_datasets"
434
- with _registry_lock:
435
- return [dict(item) for item in _custom_registry.get(key, [])]
436
-
437
-
438
- def _add_custom_hf_item(kind: Literal["models", "datasets"], payload: Dict) -> Dict:
439
- key = "hf_models" if kind == "models" else "hf_datasets"
440
- identifier = payload.get("id") or payload.get("name")
441
- if not identifier:
442
- raise ValueError("id is required")
443
- with _registry_lock:
444
- collection = _custom_registry.setdefault(key, [])
445
- if any((item.get("id") or item.get("name")) == identifier for item in collection):
446
- raise ValueError("Item already exists")
447
- collection.append(payload)
448
- _save_custom_registry()
449
- return payload
450
-
451
-
452
- def _remove_custom_hf_item(kind: Literal["models", "datasets"], identifier: str) -> bool:
453
- key = "hf_models" if kind == "models" else "hf_datasets"
454
- removed = False
455
- with _registry_lock:
456
- collection = _custom_registry.setdefault(key, [])
457
- filtered = []
458
- for item in collection:
459
- if (item.get("id") or item.get("name")) == identifier:
460
- removed = True
461
- continue
462
- filtered.append(item)
463
- if removed:
464
- _custom_registry[key] = filtered
465
- _save_custom_registry()
466
- return removed
467
-
468
-
469
- def assemble_providers() -> List[Dict]:
470
- providers: List[Dict] = []
471
- seen = set()
472
-
473
- for category, provider_list in API_PROVIDERS.items():
474
- for provider in provider_list:
475
- entry = {
476
- "name": provider["name"],
477
- "category": category,
478
- "base_url": provider["base_url"],
479
- "endpoints": provider.get("endpoints", {}),
480
- "health_endpoint": provider.get("health_endpoint"),
481
- "requires_key": False,
482
- "api_key": None,
483
- "timeout_ms": 10000
484
- }
485
-
486
- cfg = global_config.get_provider(provider["name"])
487
- if cfg:
488
- entry["health_endpoint"] = cfg.health_check_endpoint
489
- entry["requires_key"] = cfg.requires_key
490
- entry["api_key"] = cfg.api_key
491
- entry["timeout_ms"] = cfg.timeout_ms
492
-
493
- providers.append(entry)
494
- seen.add(provider_slug(provider["name"]))
495
-
496
- for cfg in global_config.get_all_providers():
497
- slug = provider_slug(cfg.name)
498
- if slug in seen:
499
- continue
500
-
501
- providers.append({
502
- "name": cfg.name,
503
- "category": cfg.category,
504
- "base_url": cfg.endpoint_url,
505
- "endpoints": {},
506
- "health_endpoint": cfg.health_check_endpoint,
507
- "requires_key": cfg.requires_key,
508
- "api_key": cfg.api_key,
509
- "timeout_ms": cfg.timeout_ms
510
- })
511
-
512
- for custom in _get_custom_providers():
513
- slug = provider_slug(custom.get("name", ""))
514
- if not slug or slug in seen:
515
- continue
516
- providers.append({
517
- "name": custom.get("name"),
518
- "category": custom.get("category", "custom"),
519
- "base_url": custom.get("base_url") or custom.get("endpoint_url"),
520
- "endpoints": custom.get("endpoints", {}),
521
- "health_endpoint": custom.get("health_endpoint") or custom.get("base_url"),
522
- "requires_key": custom.get("requires_key", False),
523
- "api_key": custom.get("api_key"),
524
- "timeout_ms": custom.get("timeout_ms", 10000),
525
- "rate_limit": custom.get("rate_limit"),
526
- "notes": custom.get("notes"),
527
- })
528
- seen.add(slug)
529
-
530
- return providers
531
-
532
- # Cache for API responses
533
  cache = {
534
  "market_data": {"data": None, "timestamp": None, "ttl": 60},
535
- "news": {"data": None, "timestamp": None, "ttl": 300},
536
  "sentiment": {"data": None, "timestamp": None, "ttl": 3600},
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/',
545
- 'https://corsproxy.io/?',
546
- ]
547
-
548
- def should_use_proxy(provider_name: str) -> bool:
549
- if not is_feature_enabled("enableProxyAutoMode"):
550
- return False
551
 
552
- cached = provider_proxy_cache.get(provider_name)
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(),
566
- "reason": "Network error or CORS issue"
567
- }
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
587
- return None
588
- except Exception as e:
589
- logger.debug(f"Proxy fetch failed for {url}: {e}")
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...")
610
- return await fetch_with_proxy(session, url)
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)
623
- logger.info(f"Network error on {url} ({e}), trying proxy...")
624
- return await fetch_with_proxy(session, url)
625
- if attempt == retries - 1:
626
- logger.debug(f"Error fetching {url}: {e}")
627
- return None
628
- await asyncio.sleep(1)
629
  except Exception as e:
630
  if attempt == retries - 1:
631
  logger.debug(f"Error fetching {url}: {e}")
632
  return None
633
  await asyncio.sleep(1)
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
 
654
  if data:
@@ -668,70 +369,6 @@ async def get_market_data():
668
  cache["market_data"]["data"] = formatted_data
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
-
675
- if data and "data" in data:
676
- formatted_data = []
677
- for coin in data["data"]:
678
- formatted_data.append({
679
- "symbol": coin.get("symbol", "").upper(),
680
- "name": coin.get("name", ""),
681
- "price": float(coin.get("priceUsd", 0)),
682
- "change_24h": float(coin.get("changePercent24Hr", 0)),
683
- "market_cap": float(coin.get("marketCapUsd", 0)),
684
- "volume_24h": float(coin.get("volumeUsd24Hr", 0)),
685
- "rank": int(coin.get("rank", 0)),
686
- "image": ""
687
- })
688
-
689
- cache["market_data"]["data"] = formatted_data
690
- cache["market_data"]["timestamp"] = datetime.now()
691
- return formatted_data
692
-
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
-
700
- if data and "data" in data:
701
- global_data = data["data"]
702
- return {
703
- "total_market_cap": global_data.get("total_market_cap", {}).get("usd", 0),
704
- "total_volume": global_data.get("total_volume", {}).get("usd", 0),
705
- "btc_dominance": global_data.get("market_cap_percentage", {}).get("btc", 0),
706
- "eth_dominance": global_data.get("market_cap_percentage", {}).get("eth", 0),
707
- "active_cryptocurrencies": global_data.get("active_cryptocurrencies", 0),
708
- "markets": global_data.get("markets", 0)
709
- }
710
-
711
- return {
712
- "total_market_cap": 0,
713
- "total_volume": 0,
714
- "btc_dominance": 0,
715
- "eth_dominance": 0,
716
- "active_cryptocurrencies": 0,
717
- "markets": 0
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)
724
-
725
- if data and "coins" in data:
726
- return [
727
- {
728
- "name": coin["item"].get("name", ""),
729
- "symbol": coin["item"].get("symbol", "").upper(),
730
- "rank": coin["item"].get("market_cap_rank", 0),
731
- "thumb": coin["item"].get("thumb", "")
732
- }
733
- for coin in data["coins"][:7]
734
- ]
735
 
736
  return []
737
 
@@ -756,237 +393,98 @@ async def get_sentiment():
756
 
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
-
763
- async with aiohttp.ClientSession() as session:
764
- url = "https://api.llama.fi/protocols"
765
- data = await fetch_with_retry(session, url)
766
-
767
- if data and isinstance(data, list):
768
- top_protocols = sorted(data, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
769
- result = [
770
- {
771
- "name": p.get("name", ""),
772
- "tvl": p.get("tvl", 0),
773
- "change_24h": p.get("change_1d", 0),
774
- "chain": p.get("chain", "")
775
- }
776
- for p in top_protocols
777
- ]
778
- cache["defi"]["data"] = result
779
- cache["defi"]["timestamp"] = datetime.now()
780
- return result
781
-
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:
788
- age = (datetime.now() - cached["timestamp"]).total_seconds()
789
- if age < HEALTH_CACHE_TTL:
790
- return cached["data"]
791
 
792
- health_config = HEALTH_TESTS.get(name, {})
793
- health_endpoint = provider.get("health_endpoint") or health_config.get("path")
794
- if not health_endpoint:
795
- endpoints = provider.get("endpoints", {})
796
- health_endpoint = next(iter(endpoints.values()), "/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
- params = dict(health_config.get("params", {}))
799
- headers = {
800
- "User-Agent": "CryptoMonitor/1.0 (+https://github.com/nimazasinich/crypto-dt-source)"
 
 
 
 
 
 
 
 
 
801
  }
802
 
803
- requires_key = provider.get("requires_key", False)
804
- api_key = provider.get("api_key")
805
- cfg = global_config.get_provider(name)
806
- if cfg:
807
- requires_key = cfg.requires_key
808
- if not api_key:
809
- api_key = cfg.api_key
810
-
811
- if health_endpoint.startswith("http"):
812
- url = health_endpoint
813
- else:
814
- url = urljoin(provider["base_url"].rstrip("/") + "/", health_endpoint.lstrip("/"))
815
-
816
- if requires_key:
817
- if not api_key:
818
- result = {
819
- "name": name,
820
- "category": provider["category"],
821
- "base_url": provider["base_url"],
822
- "status": "degraded",
823
- "uptime": db.get_uptime_percentage(name),
824
- "response_time_ms": None,
825
- "rate_limit": "",
826
- "endpoints": len(provider.get("endpoints", {})),
827
- "last_fetch": datetime.now().isoformat(),
828
- "last_check": datetime.now().isoformat(),
829
- "message": "API key not configured"
830
- }
831
- provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
832
- db.log_provider_status(name, provider["category"], "degraded", endpoint_tested=url, error_message="missing_api_key")
833
- return result
834
-
835
- header_mapping = KEY_HEADER_MAP.get(name)
836
- if header_mapping:
837
- header_name, mode = header_mapping
838
- if mode == "plain":
839
- headers[header_name] = api_key
840
- elif mode == "apikey":
841
- headers[header_name] = f"Apikey {api_key}"
842
- else:
843
- query_key = KEY_QUERY_MAP.get(name)
844
- if query_key:
845
- params[query_key] = api_key
846
- else:
847
- headers["Authorization"] = f"Bearer {api_key}"
848
-
849
- timeout_total = max(provider.get("timeout_ms", 10000) / 1000, 5)
850
- timeout = aiohttp.ClientTimeout(total=timeout_total)
851
- loop = asyncio.get_running_loop()
852
- start_time = loop.time()
853
-
854
- status = "offline"
855
- status_code = None
856
- error_message = None
857
- response_time_ms = None
858
-
859
- try:
860
- async with session.get(url, params=params, headers=headers, timeout=timeout) as response:
861
- status_code = response.status
862
- response_time_ms = round((loop.time() - start_time) * 1000, 2)
863
-
864
- if status_code < 400:
865
- status = "online"
866
- elif status_code < 500:
867
- status = "degraded"
868
- else:
869
- status = "offline"
870
-
871
- if status != "online":
872
- try:
873
- error_message = await response.text()
874
- except Exception:
875
- error_message = f"HTTP {status_code}"
876
- except Exception as exc:
877
- status = "offline"
878
- error_message = str(exc)
879
-
880
- db.log_provider_status(
881
- name,
882
- provider["category"],
883
- status,
884
- response_time=response_time_ms,
885
- status_code=status_code,
886
- endpoint_tested=url,
887
- error_message=error_message[:500] if error_message else None
888
- )
889
-
890
- uptime = db.get_uptime_percentage(name)
891
- avg_response = db.get_avg_response_time(name)
892
-
893
- result = {
894
- "name": name,
895
- "category": provider["category"],
896
- "base_url": provider["base_url"],
897
- "status": status,
898
- "uptime": uptime,
899
- "response_time_ms": response_time_ms,
900
- "avg_response_time_ms": avg_response,
901
- "rate_limit": provider.get("rate_limit", ""),
902
- "endpoints": len(provider.get("endpoints", {})),
903
- "last_fetch": datetime.now().isoformat(),
904
- "last_check": datetime.now().isoformat(),
905
- "status_code": status_code,
906
- "message": error_message[:200] if error_message else None
907
- }
908
-
909
- provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
910
- return result
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(
917
- *(fetch_provider_health(session, provider, force_refresh) for provider in providers)
918
- )
919
- return results
920
-
921
- # API Endpoints
922
-
923
  @app.get("/api/info")
924
  async def api_info():
925
- total_providers = sum(len(providers) for providers in API_PROVIDERS.values())
926
  return {
927
  "name": "Crypto Monitor Ultimate",
928
  "version": "3.0.0",
929
  "description": "Real-time crypto monitoring with 100+ free APIs",
930
- "total_providers": total_providers,
931
  "categories": list(API_PROVIDERS.keys()),
932
- "features": [
933
- "Real market data from CoinGecko, CoinCap",
934
- "Live exchange data from Binance, Coinbase, Kraken",
935
- "Crypto news aggregation",
936
- "Fear & Greed Index sentiment",
937
- "DeFi TVL tracking",
938
- "Blockchain explorer integration",
939
- "Real-time WebSocket updates"
940
- ]
941
- }
942
-
943
- @app.get("/health")
944
- async def health():
945
- providers = await get_provider_stats()
946
- total = len(providers)
947
- online = len([p for p in providers if p["status"] == "online"])
948
- degraded = len([p for p in providers if p["status"] == "degraded"])
949
-
950
- categories: Dict[str, int] = defaultdict(int)
951
- for provider in providers:
952
- categories[provider["category"]] += 1
953
-
954
- return {
955
- "status": "healthy" if total == 0 or online >= total * 0.8 else "degraded",
956
- "timestamp": datetime.now().isoformat(),
957
- "providers": {
958
- "total": total,
959
- "operational": online,
960
- "degraded": degraded,
961
- "offline": total - online - degraded
962
- },
963
- "categories": dict(categories)
964
  }
965
 
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
-
976
  return {
977
  "cryptocurrencies": data,
978
- "global": global_stats,
979
  "timestamp": datetime.now().isoformat(),
980
- "source": "CoinGecko/CoinCap"
981
- }
982
-
983
- @app.get("/api/trending")
984
- async def trending():
985
- data = await get_trending()
986
- return {
987
- "trending": data,
988
- "timestamp": datetime.now().isoformat(),
989
- "source": "CoinGecko"
990
  }
991
 
992
  @app.get("/api/sentiment")
@@ -998,201 +496,32 @@ async def sentiment():
998
  "source": "Alternative.me"
999
  }
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
-
1009
- if not data:
1010
- data = DEFI_FALLBACK
1011
-
1012
- total_tvl = sum(p.get("tvl", 0) for p in data)
1013
- return {
1014
- "protocols": data,
1015
- "total_tvl": total_tvl,
1016
- "timestamp": datetime.now().isoformat(),
1017
- "source": "DeFi Llama (fallback)" if data == DEFI_FALLBACK else "DeFi Llama"
1018
- }
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")
1036
- category = request.category.strip() or "custom"
1037
- endpoint_url = request.endpoint_url.strip()
1038
- if not endpoint_url:
1039
- raise HTTPException(status_code=400, detail="endpoint_url is required")
1040
-
1041
- payload = {
1042
- "name": name,
1043
- "category": category,
1044
- "base_url": endpoint_url,
1045
- "endpoint_url": endpoint_url,
1046
- "health_endpoint": request.health_check_endpoint.strip() if request.health_check_endpoint else endpoint_url,
1047
- "requires_key": request.requires_key,
1048
- "api_key": request.api_key.strip() if request.api_key else None,
1049
- "timeout_ms": request.timeout_ms,
1050
- "rate_limit": request.rate_limit.strip() if request.rate_limit else None,
1051
- "notes": request.notes.strip() if request.notes else None,
1052
- "created_at": datetime.utcnow().isoformat(),
1053
- }
1054
- try:
1055
- created = _add_custom_provider(payload)
1056
- except ValueError as exc:
1057
- raise HTTPException(status_code=400, detail=str(exc))
1058
-
1059
- return {"message": "Provider registered", "provider": created}
1060
-
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"])
1073
- degraded = len([p for p in providers if p.get("status") == "degraded"])
1074
- avg_response = 0.0
1075
- if providers:
1076
- response_values = [
1077
- p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
1078
- for p in providers
1079
- ]
1080
- avg_response = sum(response_values) / len(response_values)
1081
-
1082
  return {
1083
  "total_providers": len(providers),
1084
- "online": online,
1085
- "offline": offline,
1086
- "degraded": degraded,
1087
- "avg_response_time_ms": round(avg_response, 1),
1088
- "system_health": "healthy" if not providers or online >= len(providers) * 0.8 else "degraded",
1089
- "timestamp": datetime.now().isoformat()
1090
- }
1091
-
1092
-
1093
- @app.get("/status", include_in_schema=False)
1094
- async def status_legacy():
1095
- return await status()
1096
-
1097
-
1098
- @app.get("/info", include_in_schema=False)
1099
- async def info_legacy():
1100
- return await api_info()
1101
-
1102
-
1103
- @app.get("/system/info", include_in_schema=False)
1104
- async def system_info():
1105
- return await api_info()
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()
1112
- sentiment_data = await get_sentiment()
1113
-
1114
- return {
1115
- "market": {
1116
- "total_market_cap": global_stats["total_market_cap"],
1117
- "total_volume": global_stats["total_volume"],
1118
- "btc_dominance": global_stats["btc_dominance"],
1119
- "active_cryptos": global_stats["active_cryptocurrencies"],
1120
- "top_crypto_count": len(market)
1121
- },
1122
- "sentiment": {
1123
- "fear_greed_value": sentiment_data["value"],
1124
- "classification": sentiment_data["classification"]
1125
- },
1126
- "providers": {
1127
- "total": len(providers),
1128
- "operational": len([p for p in providers if p["status"] == "online"]),
1129
- "degraded": len([p for p in providers if p["status"] == "degraded"]),
1130
- "avg_uptime": round(sum(p.get("uptime", 0) for p in providers) / len(providers), 2) if providers else 0,
1131
- "avg_response_time": round(
1132
- sum((p.get("avg_response_time_ms") or p.get("response_time_ms") or 0) for p in providers) / len(providers),
1133
- 1
1134
- ) if providers else 0
1135
- },
1136
- "timestamp": datetime.now().isoformat()
1137
- }
1138
-
1139
- @app.get("/api/hf/health")
1140
- async def hf_health():
1141
- return {
1142
- "status": "healthy",
1143
- "model_loaded": True,
1144
- "timestamp": datetime.now().isoformat()
1145
- }
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"]
1158
-
1159
- positive_score = sum(1 for word in positive_words if word in text_lower)
1160
- negative_score = sum(1 for word in negative_words if word in text_lower)
1161
-
1162
- sentiment_score = (positive_score - negative_score) / max(len(text.split()), 1)
1163
- total_vote += sentiment_score
1164
-
1165
- results.append({
1166
- "text": text,
1167
- "sentiment": "positive" if sentiment_score > 0 else "negative" if sentiment_score < 0 else "neutral",
1168
- "score": round(sentiment_score, 3)
1169
- })
1170
-
1171
- avg_vote = total_vote / len(texts) if texts else 0
1172
-
1173
- return {
1174
- "vote": round(avg_vote, 3),
1175
- "results": results,
1176
  "timestamp": datetime.now().isoformat()
1177
  }
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)),
1190
- "message": "Connected to Crypto Monitor WebSocket"
1191
  })
1192
 
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({
@@ -1201,1075 +530,35 @@ async def websocket_endpoint(websocket: WebSocket):
1201
  "timestamp": datetime.now().isoformat()
1202
  })
1203
 
1204
- if random.random() > 0.8:
1205
- sentiment_data = await get_sentiment()
1206
- await websocket.send_json({
1207
- "type": "sentiment_update",
1208
- "data": sentiment_data,
1209
- "timestamp": datetime.now().isoformat()
1210
- })
1211
-
1212
  except WebSocketDisconnect:
1213
  manager.disconnect(websocket)
1214
- except Exception as exc:
1215
- manager.disconnect(websocket)
1216
- logger.debug("WebSocket session ended: %s", exc)
1217
-
1218
-
1219
- @app.websocket("/api/ws/live")
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:
1226
- with open("unified_dashboard.html", "r", encoding="utf-8") as f:
1227
- return HTMLResponse(content=f.read())
1228
- except:
1229
- try:
1230
- with open("index.html", "r", encoding="utf-8") as f:
1231
- return HTMLResponse(content=f.read())
1232
- except:
1233
- return HTMLResponse("<h1>Dashboard not found</h1>", 404)
1234
-
1235
- @app.get("/unified", response_class=HTMLResponse)
1236
- async def unified_dashboard():
1237
- try:
1238
- with open("unified_dashboard.html", "r", encoding="utf-8") as f:
1239
- return HTMLResponse(content=f.read())
1240
- except:
1241
- return HTMLResponse("<h1>Unified Dashboard not found</h1>", 404)
1242
-
1243
- @app.get("/dashboard", response_class=HTMLResponse)
1244
- async def dashboard():
1245
- try:
1246
- with open("index.html", "r", encoding="utf-8") as f:
1247
- return HTMLResponse(content=f.read())
1248
- except:
1249
- return HTMLResponse("<h1>Dashboard not found</h1>", 404)
1250
-
1251
- @app.get("/dashboard.html", response_class=HTMLResponse)
1252
- async def dashboard_html():
1253
- try:
1254
- with open("dashboard.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("/enhanced_dashboard.html", response_class=HTMLResponse)
1260
- async def enhanced_dashboard():
1261
- try:
1262
- with open("enhanced_dashboard.html", "r", encoding="utf-8") as f:
1263
- return HTMLResponse(content=f.read())
1264
- except:
1265
- return HTMLResponse("<h1>Enhanced Dashboard not found</h1>", 404)
1266
-
1267
- @app.get("/admin.html", response_class=HTMLResponse)
1268
- async def admin():
1269
- try:
1270
- with open("admin.html", "r", encoding="utf-8") as f:
1271
- return HTMLResponse(content=f.read())
1272
- except:
1273
- return HTMLResponse("<h1>Admin Panel not found</h1>", 404)
1274
-
1275
- @app.get("/hf_console.html", response_class=HTMLResponse)
1276
- async def hf_console():
1277
- try:
1278
- with open("hf_console.html", "r", encoding="utf-8") as f:
1279
- return HTMLResponse(content=f.read())
1280
- except:
1281
- return HTMLResponse("<h1>HF Console not found</h1>", 404)
1282
-
1283
- @app.get("/pool_management.html", response_class=HTMLResponse)
1284
- async def pool_management():
1285
- try:
1286
- with open("pool_management.html", "r", encoding="utf-8") as f:
1287
- return HTMLResponse(content=f.read())
1288
- except:
1289
- return HTMLResponse("<h1>Pool Management not found</h1>", 404)
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:
1298
- cat = p.get("category", "uncategorized")
1299
- entry = categories_map.setdefault(cat, {
1300
- "name": cat,
1301
- "total_sources": 0,
1302
- "online": 0,
1303
- "health_percentage": 0.0,
1304
- "avg_response": 0.0,
1305
- "last_updated": None,
1306
- "status": "unknown",
1307
- })
1308
- entry["total_sources"] += 1
1309
- if p.get("status") == "online":
1310
- entry["online"] += 1
1311
- resp = p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
1312
- entry["avg_response"] += resp
1313
- last_check = p.get("last_check") or p.get("last_fetch")
1314
- if last_check:
1315
- if not entry["last_updated"] or last_check > entry["last_updated"]:
1316
- entry["last_updated"] = last_check
1317
-
1318
- results = []
1319
- for cat, entry in categories_map.items():
1320
- total = max(entry["total_sources"], 1)
1321
- online = entry["online"]
1322
- health_pct = (online / total) * 100.0
1323
- avg_response = entry["avg_response"] / total if entry["total_sources"] else 0.0
1324
- if health_pct >= 80:
1325
- status = "healthy"
1326
- elif health_pct >= 50:
1327
- status = "degraded"
1328
- else:
1329
- status = "critical"
1330
- results.append({
1331
- "name": entry["name"],
1332
- "total_sources": total,
1333
- "online": online,
1334
- "health_percentage": round(health_pct, 2),
1335
- "avg_response": round(avg_response, 1),
1336
- "last_updated": entry["last_updated"] or datetime.now().isoformat(),
1337
- "status": status,
1338
- })
1339
- return results
1340
-
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 = []
1347
- for p in providers:
1348
- rate_str = p.get("rate_limit") or ""
1349
- limit_val = 0
1350
- window = "unknown"
1351
- if rate_str and rate_str.lower() != "unlimited":
1352
- parts = rate_str.split("/")
1353
- try:
1354
- limit_val = int("".join(ch for ch in parts[0] if ch.isdigit()))
1355
- except ValueError:
1356
- limit_val = 0
1357
- if len(parts) > 1:
1358
- window = parts[1]
1359
- elif rate_str.lower() == "unlimited":
1360
- limit_val = 0
1361
- window = "unlimited"
1362
-
1363
- status = p.get("status") or "unknown"
1364
- if limit_val > 0:
1365
- if status == "online":
1366
- used = int(limit_val * 0.4)
1367
- elif status == "degraded":
1368
- used = int(limit_val * 0.7)
1369
- else:
1370
- used = int(limit_val * 0.1)
1371
- else:
1372
- used = 0
1373
-
1374
- success_rate = p.get("uptime") or 0.0
1375
- error_rate = max(0.0, 100.0 - success_rate)
1376
- items.append({
1377
- "provider": p.get("name"),
1378
- "category": p.get("category"),
1379
- "plan": "free-tier",
1380
- "used": used,
1381
- "limit": limit_val,
1382
- "window": window,
1383
- "reset_time": (now + timedelta(minutes=15)).isoformat(),
1384
- "success_rate": round(success_rate, 2),
1385
- "error_rate": round(error_rate, 2),
1386
- "avg_response": round(p.get("avg_response_time_ms") or 0.0, 1),
1387
- "last_checked": p.get("last_check") or now.isoformat(),
1388
- "notes": f"Status: {status}",
1389
- })
1390
- return items
1391
-
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:
1398
- status = row.get("status") or "unknown"
1399
- is_error = status != "online"
1400
- if type == "errors" and not is_error:
1401
- continue
1402
- if type == "incidents" and not is_error:
1403
- continue
1404
- msg = row.get("error_message") or ""
1405
- if not msg and row.get("status_code"):
1406
- msg = f"HTTP {row['status_code']} on {row.get('endpoint_tested') or ''}".strip()
1407
- logs.append({
1408
- "timestamp": row.get("timestamp") or row.get("created_at"),
1409
- "provider": row.get("provider_name") or "System",
1410
- "type": "error" if is_error else "info",
1411
- "status": status,
1412
- "response_time": row.get("response_time"),
1413
- "message": msg or "No message",
1414
- })
1415
- return logs
1416
-
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)
1423
- last_error = None
1424
- for row in rows:
1425
- status = (row.get("status") or "unknown").lower()
1426
- provider = row.get("provider_name") or "System"
1427
- by_status[status] += 1
1428
- by_provider[provider] += 1
1429
- if status != "online":
1430
- last_error = last_error or {
1431
- "provider": provider,
1432
- "status": status,
1433
- "timestamp": row.get("timestamp") or row.get("created_at"),
1434
- "message": row.get("error_message") or row.get("status_code"),
1435
- }
1436
- return {
1437
- "total": len(rows),
1438
- "by_status": dict(by_status),
1439
- "by_provider": dict(sorted(by_provider.items(), key=lambda item: item[1], reverse=True)[:8]),
1440
- "last_error": last_error,
1441
- "hours": hours,
1442
- }
1443
-
1444
-
1445
- @app.get("/api/alerts")
1446
- async def api_alerts():
1447
- try:
1448
- rows = db.get_unacknowledged_alerts()
1449
  except Exception:
1450
- return []
1451
- alerts = []
1452
- for row in rows:
1453
- severity = row.get("alert_type") or "warning"
1454
- provider = row.get("provider_name") or "System"
1455
- title = f"{severity.title()} alert - {provider}"
1456
- alerts.append({
1457
- "severity": severity.lower(),
1458
- "title": title,
1459
- "timestamp": row.get("triggered_at") or datetime.now().isoformat(),
1460
- "message": row.get("message") or "",
1461
- "provider": provider,
1462
- })
1463
- return alerts
1464
-
1465
-
1466
-
1467
- HF_MODELS: List[Dict] = []
1468
- HF_DATASETS: List[Dict] = []
1469
- HF_CACHE_TS: Optional[datetime] = None
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:
1478
- return HF_MODELS
1479
- if kind == "datasets" and HF_DATASETS:
1480
- return HF_DATASETS
1481
-
1482
- base_url = "https://huggingface.co/api/models" if kind == "models" else "https://huggingface.co/api/datasets"
1483
- params = {"search": query, "limit": str(limit)}
1484
- headers: Dict[str, str] = {}
1485
- token = os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN")
1486
- if token:
1487
- headers["Authorization"] = f"Bearer {token}"
1488
-
1489
- items: List[Dict] = []
1490
- try:
1491
- async with aiohttp.ClientSession() as session:
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"),
1498
- "description": entry.get("pipeline_tag")
1499
- or entry.get("cardData", {}).get("summary")
1500
- or entry.get("description", ""),
1501
- "downloads": entry.get("downloads", 0),
1502
- "likes": entry.get("likes", 0),
1503
- }
1504
- items.append(item)
1505
- except Exception:
1506
- items = []
1507
-
1508
- if not items:
1509
- if kind == "models":
1510
- items = [
1511
- {
1512
- "id": "distilbert-base-uncased-finetuned-sst-2-english",
1513
- "description": "English sentiment analysis model (SST-2).",
1514
- "downloads": 100000,
1515
- "likes": 1200,
1516
- },
1517
- {
1518
- "id": "bert-base-multilingual-cased",
1519
- "description": "Multilingual BERT model suitable for many languages.",
1520
- "downloads": 500000,
1521
- "likes": 4000,
1522
- },
1523
- ]
1524
- else:
1525
- items = [
1526
- {
1527
- "id": "crypto-sentiment-demo",
1528
- "description": "Synthetic crypto sentiment dataset for demo purposes.",
1529
- "downloads": 1200,
1530
- "likes": 40,
1531
- },
1532
- {
1533
- "id": "financial-news-sample",
1534
- "description": "Sample of financial news headlines.",
1535
- "downloads": 800,
1536
- "likes": 25,
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}
1543
- for custom in custom_items:
1544
- identifier = custom.get("id") or custom.get("name")
1545
- if identifier in seen_ids:
1546
- continue
1547
- items.append(custom)
1548
- seen_ids.add(identifier)
1549
-
1550
- if kind == "models":
1551
- HF_MODELS = items
1552
- else:
1553
- HF_DATASETS = items
1554
- HF_CACHE_TS = now
1555
- return items
1556
-
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)}
1563
-
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:
1570
- data = await _fetch_hf_registry("models")
1571
- return data
1572
-
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"),
1579
- }
1580
-
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 "",
1587
- "downloads": item.downloads or 0,
1588
- "likes": item.likes or 0,
1589
- "created_at": datetime.utcnow().isoformat(),
1590
- }
1591
- target_kind: Literal["models", "datasets"] = "models" if item.kind == "model" else "datasets"
1592
- try:
1593
- created = _add_custom_hf_item(target_kind, payload)
1594
- except ValueError as exc:
1595
- raise HTTPException(status_code=400, detail=str(exc))
1596
- return {"message": "Item added", "item": created}
1597
-
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'")
1604
- decoded = unquote(identifier)
1605
- if not _remove_custom_hf_item("models" if kind == "model" else "datasets", decoded):
1606
- raise HTTPException(status_code=404, detail="Item not found")
1607
- return Response(status_code=204)
1608
-
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] = []
1615
- for item in pool:
1616
- text = f"{item.get('id','')} {item.get('description','')}".lower()
1617
- if not q_lower or q_lower in text:
1618
- results.append(item)
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 {
1632
- "success": True,
1633
- "message": f"Updated {len(request.flags)} feature flags",
1634
- "flags": feature_flags.get_all_flags()
1635
- }
1636
- else:
1637
- raise HTTPException(status_code=500, detail="Failed to update feature flags")
1638
-
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 {
1645
- "success": True,
1646
- "message": f"Feature flag '{flag_name}' set to {request.value}",
1647
- "flag_name": flag_name,
1648
- "value": request.value
1649
- }
1650
- else:
1651
- raise HTTPException(status_code=500, detail="Failed to update feature flag")
1652
-
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 {
1659
- "success": True,
1660
- "message": "Feature flags reset to defaults",
1661
- "flags": feature_flags.get_all_flags()
1662
- }
1663
- else:
1664
- raise HTTPException(status_code=500, detail="Failed to reset feature flags")
1665
-
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,
1672
- "value": value,
1673
- "enabled": value
1674
- }
1675
-
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()
1682
- status.append({
1683
- "provider": provider_name,
1684
- "using_proxy": cache_data.get("use_proxy", False),
1685
- "reason": cache_data.get("reason", "Unknown"),
1686
- "cached_since": cache_data.get("timestamp", datetime.now()).isoformat(),
1687
- "cache_age_seconds": int(age_seconds)
1688
- })
1689
-
1690
- return {
1691
- "proxy_auto_mode_enabled": is_feature_enabled("enableProxyAutoMode"),
1692
- "total_providers_using_proxy": len(status),
1693
- "providers": status,
1694
- "available_proxies": CORS_PROXIES
1695
- }
1696
-
1697
-
1698
- @app.get("/providers", include_in_schema=False)
1699
- async def providers_legacy():
1700
- return await providers()
1701
-
1702
-
1703
- @app.get("/providers/health", include_in_schema=False)
1704
- async def providers_health_legacy():
1705
- data = await providers()
1706
- total = len(data)
1707
- online = len([p for p in data if p.get("status") == "online"])
1708
- degraded = len([p for p in data if p.get("status") == "degraded"])
1709
- return {
1710
- "providers": data,
1711
- "summary": {
1712
- "total": total,
1713
- "online": online,
1714
- "degraded": degraded,
1715
- "offline": total - online - degraded,
1716
- },
1717
- "timestamp": datetime.now().isoformat(),
1718
- }
1719
-
1720
-
1721
- @app.get("/categories", include_in_schema=False)
1722
- async def categories_legacy():
1723
- return await api_categories()
1724
-
1725
-
1726
- @app.get("/rate-limits", include_in_schema=False)
1727
- async def rate_limits_legacy():
1728
- return await api_rate_limits()
1729
-
1730
-
1731
- @app.get("/logs", include_in_schema=False)
1732
- async def logs_legacy(type: str = "all"):
1733
- return await api_logs(type=type)
1734
-
1735
-
1736
- @app.get("/alerts", include_in_schema=False)
1737
- async def alerts_legacy():
1738
- return await api_alerts()
1739
-
1740
-
1741
- @app.get("/hf/registry", include_in_schema=False)
1742
- async def hf_registry_legacy(type: str = "models"):
1743
- return await hf_registry(type=type)
1744
-
1745
-
1746
- @app.post("/hf/refresh", include_in_schema=False)
1747
- async def hf_refresh_legacy():
1748
- return await hf_refresh()
1749
-
1750
-
1751
- @app.get("/hf/search", include_in_schema=False)
1752
- async def hf_search_legacy(q: str = "", kind: str = "models"):
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:
1769
- with open("config.js", "r", encoding="utf-8") as f:
1770
- return Response(content=f.read(), media_type="application/javascript")
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": {
1779
- "config_loader": {
1780
- "apis_loaded": len(providers),
1781
- "status": "active"
1782
- },
1783
- "scheduler": {
1784
- "total_tasks": len(providers),
1785
- "status": "active"
1786
- },
1787
- "persistence": {
1788
- "cached_apis": len(providers),
1789
- "status": "active"
1790
- },
1791
- "websocket": {
1792
- "total_connections": len(manager.active_connections),
1793
- "status": "active"
1794
- }
1795
- },
1796
- "timestamp": datetime.now().isoformat()
1797
- }
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:
1804
- apis[p["name"].lower().replace(" ", "_")] = {
1805
- "name": p["name"],
1806
- "category": p["category"],
1807
- "base_url": p.get("base_url", ""),
1808
- "status": p["status"]
1809
- }
1810
- return {"apis": apis}
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:
1817
- api_id = p["name"].lower().replace(" ", "_")
1818
- tasks[api_id] = {
1819
- "api_id": api_id,
1820
- "interval": 300,
1821
- "enabled": True,
1822
- "last_status": "success",
1823
- "last_run": datetime.now().isoformat()
1824
- }
1825
- return tasks
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,
1832
- "enabled": True,
1833
- "last_status": "success",
1834
- "last_run": datetime.now().isoformat()
1835
- }
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,
1842
- "enabled": enabled,
1843
- "message": "Schedule updated"
1844
- }
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",
1851
- "timestamp": datetime.now().isoformat()
1852
- }
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",
1859
- "download_url": "/api/v2/export/download/export.json",
1860
- "timestamp": datetime.now().isoformat()
1861
- }
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",
1868
- "timestamp": datetime.now().isoformat()
1869
- }
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()
1876
- }
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
1883
- return {
1884
- "status": "cleared",
1885
- "timestamp": datetime.now().isoformat()
1886
- }
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": {
1898
- "timestamp": datetime.now().isoformat()
1899
- }
1900
- })
1901
-
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
1908
-
1909
- for member in pool.get("members", []):
1910
- provider_id = member["provider_id"]
1911
- provider_status = provider_map.get(provider_id)
1912
-
1913
- status = provider_status["status"] if provider_status else "unknown"
1914
- uptime = provider_status.get("uptime", member.get("success_rate", 0)) if provider_status else member.get("success_rate", 0)
1915
- response_time = provider_status.get("response_time_ms") if provider_status else None
1916
-
1917
- member_payload = {
1918
- "provider_id": provider_id,
1919
- "provider_name": member["provider_name"],
1920
- "priority": member.get("priority", 1),
1921
- "weight": member.get("weight", 1),
1922
- "use_count": member.get("use_count", 0),
1923
- "success_rate": round(uptime, 2) if uptime is not None else 0,
1924
- "status": status,
1925
- "response_time_ms": response_time,
1926
- "rate_limit": {
1927
- "usage": member.get("rate_limit_usage", 0),
1928
- "limit": member.get("rate_limit_limit", 0),
1929
- "percentage": member.get("rate_limit_percentage", 0)
1930
- }
1931
- }
1932
-
1933
- db.update_member_stats(
1934
- pool["id"],
1935
- provider_id,
1936
- success_rate=uptime,
1937
- rate_limit_usage=member_payload["rate_limit"]["usage"],
1938
- rate_limit_limit=member_payload["rate_limit"]["limit"],
1939
- rate_limit_percentage=member_payload["rate_limit"]["percentage"],
1940
- )
1941
-
1942
- members_payload.append(member_payload)
1943
-
1944
- if not current_provider and status == "online":
1945
- current_provider = {"name": member["provider_name"], "status": status}
1946
-
1947
- if not current_provider and members_payload:
1948
- degraded_member = next((m for m in members_payload if m["status"] == "degraded"), None)
1949
- if degraded_member:
1950
- current_provider = {"name": degraded_member["provider_name"], "status": degraded_member["status"]}
1951
-
1952
- return {
1953
- "pool_id": pool["id"],
1954
- "pool_name": pool["name"],
1955
- "category": pool["category"],
1956
- "rotation_strategy": pool["rotation_strategy"],
1957
- "description": pool.get("description"),
1958
- "enabled": bool(pool.get("enabled", 1)),
1959
- "members": members_payload,
1960
- "current_provider": current_provider,
1961
- "total_rotations": pool.get("rotation_count", 0),
1962
- "created_at": pool.get("created_at")
1963
- }
1964
-
1965
-
1966
- def transform_rotation_history(entries: List[Dict]) -> List[Dict]:
1967
- history = []
1968
- for entry in entries:
1969
- history.append({
1970
- "pool_id": entry["pool_id"],
1971
- "provider_id": entry["provider_id"],
1972
- "provider_name": entry["provider_name"],
1973
- "reason": entry["reason"],
1974
- "timestamp": entry["created_at"]
1975
- })
1976
- return history
1977
-
1978
-
1979
- async def broadcast_pool_update(action: str, pool_id: int, extra: Optional[Dict] = None):
1980
- payload = {"type": "pool_update", "action": action, "pool_id": pool_id}
1981
- if extra:
1982
- payload.update(extra)
1983
- await manager.broadcast(payload)
1984
-
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()
1991
- response = [build_pool_payload(pool, provider_map) for pool in pools]
1992
- return {"pools": response}
1993
-
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")
2000
-
2001
- pool_id = db.create_pool(
2002
- name=pool.name,
2003
- category=pool.category,
2004
- rotation_strategy=pool.rotation_strategy,
2005
- description=pool.description,
2006
- enabled=True
2007
- )
2008
-
2009
- providers = await get_provider_stats()
2010
- provider_map = {provider_slug(p["name"]): p for p in providers}
2011
- pool_record = db.get_pool(pool_id)
2012
- payload = build_pool_payload(pool_record, provider_map)
2013
-
2014
- await broadcast_pool_update("created", pool_id, {"pool": payload})
2015
-
2016
- return {
2017
- "pool_id": pool_id,
2018
- "message": "Pool created successfully",
2019
- "pool": payload
2020
- }
2021
-
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")
2028
-
2029
- providers = await get_provider_stats()
2030
- provider_map = {provider_slug(p["name"]): p for p in providers}
2031
- return build_pool_payload(pool, provider_map)
2032
-
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")
2039
-
2040
- db.delete_pool(pool_id)
2041
- await broadcast_pool_update("deleted", pool_id)
2042
- return {"message": "Pool deleted successfully"}
2043
-
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")
2050
-
2051
- providers = await get_provider_stats()
2052
- provider_map = {provider_slug(p["name"]): p for p in providers}
2053
- provider_info = provider_map.get(member.provider_id)
2054
- if not provider_info:
2055
- raise HTTPException(status_code=404, detail="Provider not found")
2056
-
2057
- existing = next((m for m in pool["members"] if m["provider_id"] == member.provider_id), None)
2058
- if existing:
2059
- raise HTTPException(status_code=400, detail="Provider already in pool")
2060
-
2061
- db.add_pool_member(
2062
- pool_id=pool_id,
2063
- provider_id=member.provider_id,
2064
- provider_name=provider_info["name"],
2065
- priority=max(1, min(member.priority, 10)),
2066
- weight=max(1, min(member.weight, 100)),
2067
- success_rate=provider_info.get("uptime", 0),
2068
- rate_limit_usage=provider_info.get("rate_limit", {}).get("usage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2069
- rate_limit_limit=provider_info.get("rate_limit", {}).get("limit", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2070
- rate_limit_percentage=provider_info.get("rate_limit", {}).get("percentage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
2071
- )
2072
-
2073
- pool_record = db.get_pool(pool_id)
2074
- payload = build_pool_payload(pool_record, provider_map)
2075
- await broadcast_pool_update("member_added", pool_id, {"provider_id": member.provider_id})
2076
-
2077
- return {
2078
- "message": "Member added successfully",
2079
- "pool": payload
2080
- }
2081
-
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")
2088
-
2089
- db.remove_pool_member(pool_id, provider_id)
2090
- await broadcast_pool_update("member_removed", pool_id, {"provider_id": provider_id})
2091
-
2092
- providers = await get_provider_stats()
2093
- provider_map = {provider_slug(p["name"]): p for p in providers}
2094
- pool_record = db.get_pool(pool_id)
2095
- payload = build_pool_payload(pool_record, provider_map)
2096
-
2097
- return {
2098
- "message": "Member removed successfully",
2099
- "pool": payload
2100
- }
2101
-
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")
2108
-
2109
- if not pool["members"]:
2110
- raise HTTPException(status_code=400, detail="Pool has no members")
2111
-
2112
- providers = await get_provider_stats(force_refresh=True)
2113
- provider_map = {provider_slug(p["name"]): p for p in providers}
2114
-
2115
- members_with_status = []
2116
- for member in pool["members"]:
2117
- status_info = provider_map.get(member["provider_id"])
2118
- if status_info:
2119
- members_with_status.append((member, status_info))
2120
-
2121
- online_members = [m for m in members_with_status if m[1]["status"] == "online"]
2122
- degraded_members = [m for m in members_with_status if m[1]["status"] == "degraded"]
2123
-
2124
- candidates = online_members or degraded_members
2125
- if not candidates:
2126
- raise HTTPException(status_code=400, detail="No healthy providers available for rotation")
2127
-
2128
- strategy = pool.get("rotation_strategy", "round_robin")
2129
-
2130
- if strategy == "priority":
2131
- candidates.sort(key=lambda x: (x[0].get("priority", 1), x[0].get("weight", 1)), reverse=True)
2132
- selected_member, status_info = candidates[0]
2133
- elif strategy == "weighted":
2134
- weights = [max(1, c[0].get("weight", 1)) for c in candidates]
2135
- total_weight = sum(weights)
2136
- roll = random.uniform(0, total_weight)
2137
- cumulative = 0
2138
- selected_member = candidates[0][0]
2139
- status_info = candidates[0][1]
2140
- for (candidate, status), weight in zip(candidates, weights):
2141
- cumulative += weight
2142
- if roll <= cumulative:
2143
- selected_member, status_info = candidate, status
2144
- break
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
-
2152
- db.increment_member_use(pool_id, selected_member["provider_id"])
2153
- db.update_member_stats(
2154
- pool_id,
2155
- selected_member["provider_id"],
2156
- success_rate=status_info.get("uptime", selected_member.get("success_rate")),
2157
- rate_limit_usage=status_info.get("rate_limit", {}).get("usage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2158
- rate_limit_limit=status_info.get("rate_limit", {}).get("limit", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2159
- rate_limit_percentage=status_info.get("rate_limit", {}).get("percentage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
2160
- )
2161
- db.log_pool_rotation(
2162
- pool_id,
2163
- selected_member["provider_id"],
2164
- selected_member["provider_name"],
2165
- request.get("reason", "manual") if request else "manual"
2166
- )
2167
-
2168
- pool_record = db.get_pool(pool_id)
2169
- payload = build_pool_payload(pool_record, provider_map)
2170
-
2171
- await broadcast_pool_update("rotated", pool_id, {
2172
- "provider_id": selected_member["provider_id"],
2173
- "provider_name": selected_member["provider_name"]
2174
- })
2175
-
2176
- return {
2177
- "message": "Pool rotated successfully",
2178
- "provider_name": selected_member["provider_name"],
2179
- "provider_id": selected_member["provider_id"],
2180
- "total_rotations": pool_record.get("rotation_count", 0),
2181
- "pool": payload
2182
- }
2183
-
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)
2193
- return {
2194
- "history": history,
2195
- "total": len(history)
2196
- }
2197
-
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)
2207
- return {
2208
- "history": history,
2209
- "total": len(history)
2210
- }
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:
2217
- config = json.load(f)
2218
- return config
2219
- except FileNotFoundError:
2220
- raise HTTPException(status_code=404, detail="Provider config file not found")
2221
- except json.JSONDecodeError:
2222
- raise HTTPException(status_code=500, detail="Invalid JSON in provider config")
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)
2230
-
2231
- provider = config.get('providers', {}).get(provider_id)
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"}
2238
-
2239
- import time
2240
- start_time = time.time()
2241
-
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,
2249
- "response_time": round(response_time, 2),
2250
- "http_status": response.status
2251
- }
2252
- except asyncio.TimeoutError:
2253
- return {"status": "offline", "error": "Timeout after 5s"}
2254
- except Exception as e:
2255
- return {"status": "offline", "error": str(e)}
2256
-
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,
 
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
  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
  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
  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
  "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
  "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
  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
 
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
  "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
  {
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
  cache["market_data"]["data"] = formatted_data
370
  cache["market_data"]["timestamp"] = datetime.now()
371
  return formatted_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
 
373
  return []
374
 
 
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
  "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
  "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,