Really-amin commited on
Commit
c51f130
·
verified ·
1 Parent(s): 12c73d7

Upload 3 files

Browse files
Files changed (3) hide show
  1. templates/app.py +1785 -424
  2. templates/index.html +0 -0
  3. templates/main.py +30 -0
templates/app.py CHANGED
@@ -1,529 +1,1890 @@
1
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query, Body
2
- from fastapi.responses import FileResponse, JSONResponse
 
 
 
 
 
 
 
3
  from fastapi.middleware.cors import CORSMiddleware
4
- from pathlib import Path
5
- from datetime import datetime, timedelta, timezone, date
6
- from typing import Any, Dict, List, Optional
7
  import asyncio
 
8
  import random
9
- import ccxt
10
-
11
- app = FastAPI(title="Crypto Data Source API (Dual Mode)")
12
-
13
- # CORS
14
- app.add_middleware(
15
- CORSMiddleware,
16
- allow_origins=["*"],
17
- allow_credentials=True,
18
- allow_methods=["*"],
19
- allow_headers=["*"],
20
- )
21
-
22
- BASE_DIR = Path(__file__).resolve().parent
23
- INDEX_FILE = BASE_DIR / "index.html"
24
- START_TIME = datetime.now(timezone.utc)
25
-
26
- # ------------------------------
27
- # Sample data
28
- # ------------------------------
29
- BASE_PROVIDERS: List[Dict[str, Any]] = [
30
- {"name": "Binance Spot", "category": "market_data", "has_key": False, "priority": 5, "base_url": "https://api.binance.com"},
31
- {"name": "Binance Futures", "category": "market_data", "has_key": False, "priority": 4, "base_url": "https://fapi.binance.com"},
32
- {"name": "CoinGecko", "category": "market_data", "has_key": False, "priority": 4, "base_url": "https://api.coingecko.com"},
33
- {"name": "CoinPaprika", "category": "market_data", "has_key": False, "priority": 3, "base_url": "https://api.coinpaprika.com"},
34
- {"name": "Etherscan", "category": "blockchain", "has_key": True, "priority": 4, "base_url": "https://api.etherscan.io"},
35
- {"name": "BscScan", "category": "blockchain", "has_key": True, "priority": 3, "base_url": "https://api.bscscan.com"},
36
- {"name": "TronScan", "category": "blockchain", "has_key": False, "priority": 3, "base_url": "https://apilist.tronscanapi.com"},
37
- {"name": "CryptoPanic", "category": "news", "has_key": False, "priority": 2, "base_url": "https://cryptopanic.com/api"},
38
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- CUSTOM_APIS: List[Dict[str, Any]] = []
 
 
 
41
 
42
- API_KEYS: List[Dict[str, str]] = [
43
- {"provider": "Binance", "key_masked": "BINANCE-****-****-1234"},
44
- {"provider": "Etherscan", "key_masked": "ETHERSCAN-****-****-ABCD"},
45
- {"provider": "BscScan", "key_masked": "BSCSCAN-****-****-5678"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  ]
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- def _utc_now() -> datetime:
50
- return datetime.now(timezone.utc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- def _provider_snapshot(provider: Dict[str, Any]) -> Dict[str, Any]:
54
- """Return one provider item in *rich* shape for *both* frontends."""
55
- rt = random.randint(80, 900)
56
- minutes_ago = random.uniform(0, 30)
57
- last_dt = _utc_now() - timedelta(minutes=minutes_ago)
 
 
58
 
59
- if rt < 300:
60
- status = "online"
61
- elif rt < 700:
62
- status = "degraded"
63
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  status = "offline"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- # Common fields
67
- item = {
68
- "name": provider["name"],
69
  "category": provider["category"],
 
70
  "status": status,
71
- "response_time_ms": rt,
72
- "last_fetch": last_dt.isoformat(),
73
- "has_key": bool(provider.get("has_key", False)),
74
- "priority": int(provider.get("priority", 3)),
 
 
 
 
 
75
  }
76
- # Extended fields for the other UI flavor
77
- item.update({
78
- "base_url": provider.get("base_url", ""),
79
- "response_time": rt, # alias
80
- "last_checked": last_dt.isoformat(), # alias
81
- "success_rate": round(random.uniform(80, 99), 2),
82
- "last_success": (last_dt - timedelta(minutes=random.randint(1, 10))).isoformat(),
83
- "error_count_24h": random.randint(0, 12),
84
- })
85
- return item
86
 
 
 
87
 
88
- def _generate_providers_snapshot() -> List[Dict[str, Any]]:
89
- providers = [_provider_snapshot(p) for p in (BASE_PROVIDERS + CUSTOM_APIS)]
90
- return providers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
 
93
- # ------------------------------
94
- # CCXT basic APIs
95
- # ------------------------------
96
  @app.get("/api/health")
97
  async def api_health():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  return {
99
- "status": "online",
100
- "version": "1.0.0",
101
- "timestamp": _utc_now().isoformat(),
102
- "ccxt_version": ccxt.__version__,
103
  }
104
 
 
 
 
 
 
 
 
 
 
105
 
106
- @app.get("/api/markets")
107
- async def api_markets():
 
108
  try:
109
- exchange = ccxt.binance()
110
- markets = exchange.load_markets()
111
- return {
112
- "success": True,
113
- "total_markets": len(markets),
114
- "markets": list(markets.keys())[:50],
115
- }
116
- except Exception as e:
117
- return {"success": False, "error": str(e)}
118
 
 
 
119
 
120
- @app.get("/api/ticker/{symbol}")
121
- async def api_ticker(symbol: str):
122
- try:
123
- exchange = ccxt.binance()
124
- ticker = exchange.fetch_ticker(symbol)
125
- return {"success": True, "data": ticker}
126
- except Exception as e:
127
- return {"success": False, "error": str(e)}
128
-
129
-
130
- # ------------------------------
131
- # Dual-mode status & KPI
132
- # ------------------------------
133
- def _status_payload() -> Dict[str, Any]:
134
- providers = _generate_providers_snapshot()
135
- total = len(providers)
136
- online = sum(1 for p in providers if p["status"] == "online")
137
- offline = sum(1 for p in providers if p["status"] == "offline")
138
- degraded = sum(1 for p in providers if p["status"] == "degraded")
139
- avg_response = int(sum(p["response_time_ms"] for p in providers) / total) if total else 0
140
- total_requests_hour = random.randint(200, 1200)
141
- total_failures_hour = random.randint(0, max(20, degraded * 2 + offline * 5))
142
- system_health = "healthy" if offline == 0 and degraded <= max(1, total // 4) else "degraded"
143
- now = _utc_now()
144
- uptime_seconds = (now - START_TIME).total_seconds()
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  return {
147
- "status": "online",
148
- "service": "crypto-data-source",
149
- "version": "1.0.0",
150
- "timestamp": now.isoformat(),
151
- "uptime_seconds": uptime_seconds,
152
- "total_providers": total,
153
  "online": online,
154
  "offline": offline,
155
  "degraded": degraded,
156
- "avg_response_time_ms": avg_response,
157
- "system_health": system_health,
158
- "total_requests_hour": total_requests_hour,
159
- "total_failures_hour": total_failures_hour,
160
  }
161
 
162
 
163
- @app.get("/api/status")
164
- async def api_status():
165
- return _status_payload()
166
 
167
 
168
- # Aliases for alternate frontend:
169
- @app.get("/health")
170
- async def alias_health():
171
- # Some UIs call /health (without /api)
172
- return await api_health()
173
 
174
 
175
- @app.get("/info")
176
- async def alias_info():
177
- # Some UIs call /info expecting a status-like object
178
- return _status_payload()
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- # ------------------------------
182
- # Providers & categories
183
- # ------------------------------
184
- @app.get("/api/providers")
185
- async def api_providers():
186
- return _generate_providers_snapshot()
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  @app.get("/api/categories")
190
  async def api_categories():
191
- providers = _generate_providers_snapshot()
192
- by_cat: Dict[str, List[Dict[str, Any]]] = {}
 
193
  for p in providers:
194
- by_cat.setdefault(p["category"], []).append(p)
195
-
196
- payload: List[Dict[str, Any]] = []
197
- for category, items in by_cat.items():
198
- total_sources = len(items)
199
- online_sources = sum(1 for i in items if i["status"] == "online")
200
- avg_rt = int(sum(i["response_time_ms"] for i in items) / total_sources) if total_sources else 0
201
- # minimal fields expected by both UIs:
202
- payload.append({
203
- # our previous UI:
204
- "name": category,
205
- "status": "online" if online_sources > 0 else "offline",
206
- "online_sources": online_sources,
207
- "total_sources": total_sources,
208
- "avg_response_time_ms": avg_rt,
209
- # aliases for other UI:
210
- "online": online_sources,
211
- "avg_response": avg_rt,
212
- "health_percentage": int((online_sources / total_sources) * 100) if total_sources else 0,
213
- "last_updated": _utc_now().isoformat(),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  })
215
- return payload
216
-
217
-
218
- # ------------------------------
219
- # Charts
220
- # ------------------------------
221
- @app.get("/api/charts/health-history")
222
- async def api_chart_health_history(hours: int = 24):
223
- now = _utc_now()
224
- points = max(12, hours)
225
- timestamps: List[str] = []
226
- success_rate: List[float] = []
227
- for i in range(points):
228
- ts = now - timedelta(hours=(hours * (points - 1 - i) / points))
229
- base = 90
230
- noise = random.uniform(-10, 5)
231
- value = max(50.0, min(100.0, base + noise))
232
- timestamps.append(ts.isoformat())
233
- success_rate.append(round(value, 2))
234
- return {"timestamps": timestamps, "success_rate": success_rate}
235
-
236
-
237
- @app.get("/api/charts/compliance")
238
- async def api_chart_compliance(days: int = 7):
239
- today = date.today()
240
- dates: List[str] = []
241
- compliance_percentage: List[float] = []
242
- for i in range(days):
243
- d = today - timedelta(days=(days - 1 - i))
244
- base = 88
245
- noise = random.uniform(-8, 5)
246
- value = max(60.0, min(100.0, base + noise))
247
- dates.append(d.isoformat())
248
- compliance_percentage.append(round(value, 2))
249
- return {"dates": dates, "compliance_percentage": compliance_percentage}
250
-
251
-
252
- # ------------------------------
253
- # Freshness & Logs & Failures
254
- # ------------------------------
255
- @app.get("/api/freshness")
256
- async def api_freshness():
257
- providers = _generate_providers_snapshot()
258
- items: List[Dict[str, Any]] = []
259
  for p in providers:
260
- fetch_time = datetime.fromisoformat(p["last_fetch"])
261
- staleness_minutes = (_utc_now() - fetch_time).total_seconds() / 60.0
262
- ttl_minutes = 30
263
- status = "fresh" if staleness_minutes <= ttl_minutes else "stale"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  items.append({
265
- "provider": p["name"],
266
- "category": p["category"],
267
- "fetch_time": fetch_time.isoformat(),
268
- "staleness_minutes": round(staleness_minutes, 1),
269
- "ttl_minutes": ttl_minutes,
270
- "status": status,
 
 
 
 
 
 
271
  })
272
  return items
273
 
274
 
275
- def _synth_logs() -> List[Dict[str, Any]]:
276
- providers = _generate_providers_snapshot()
277
- logs: List[Dict[str, Any]] = []
278
- now = _utc_now()
279
- for p in providers:
280
- # success
 
 
 
 
 
 
 
 
 
281
  logs.append({
282
- "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
283
- "provider": p["name"],
284
- "endpoint": "/api/status",
285
- "status": "success",
286
- "http_code": 200,
287
- "response_time_ms": p["response_time_ms"],
288
- "error_message": None,
289
- "type": "connection"
290
  })
291
- # possible error
292
- if p["status"] != "online" and random.random() < 0.5:
293
- logs.append({
294
- "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
295
- "provider": p["name"],
296
- "endpoint": "/api/markets",
297
- "status": "error",
298
- "http_code": random.choice([429, 500, 504]),
299
- "response_time_ms": random.randint(500, 2000),
300
- "error_message": "Upstream error or timeout",
301
- "type": "error"
302
- })
303
- logs.sort(key=lambda x: x["timestamp"], reverse=True)
304
  return logs
305
 
306
 
307
- @app.get("/api/logs")
308
- async def api_logs(log_type: Optional[str] = Query(None, alias="type")):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  """
310
- Optional ?type=connection|error to filter.
 
311
  """
312
- logs = _synth_logs()
313
- if log_type in {"connection", "error"}:
314
- logs = [l for l in logs if l.get("type") == log_type]
315
- return logs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
 
318
- @app.get("/api/failures")
319
- async def api_failures():
320
- logs = _synth_logs()
321
- failures: List[Dict[str, Any]] = []
322
- for log in logs:
323
- if log["status"] == "error":
324
- failures.append({
325
- "timestamp": log["timestamp"],
326
- "provider": log["provider"],
327
- "error_type": "HTTP " + str(log["http_code"]),
328
- "error_message": log["error_message"] or "Unknown error",
329
- "retry_attempted": random.random() < 0.6,
330
- })
331
- return {"recent_failures": failures}
332
 
333
 
334
- # alias for another UI calling /api/alerts
335
- @app.get("/api/alerts")
336
- async def api_alerts():
337
- return await api_failures()
 
 
 
 
338
 
339
 
340
- # ------------------------------
341
- # Rate limits (synthetic)
342
- # ------------------------------
343
- @app.get("/api/rate-limits")
344
- async def api_rate_limits():
345
- providers = _generate_providers_snapshot()
346
- out = []
347
- for p in providers:
348
- out.append({
349
- "provider": p["name"],
350
- "limit_per_min": random.choice([60, 120, 300, 600]),
351
- "used_in_min": random.randint(0, 60),
352
- "cooldown_sec": random.choice([0, 0, 0, 10, 20, 30]),
353
- "status": p["status"],
354
- })
355
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
 
358
- # ------------------------------
359
- # Config keys & custom add
360
- # ------------------------------
361
- @app.get("/api/config/keys")
362
- async def api_config_keys():
363
- return API_KEYS
364
 
365
 
366
- @app.post("/api/custom/add")
367
- async def api_custom_add(
368
- name: str = Query(...),
369
- url: str = Query(...),
370
- category: str = Query(...),
371
- test_field: Optional[str] = Query(None),
372
- ):
373
- CUSTOM_APIS.append({
374
- "name": name,
375
- "url": url,
376
- "category": category or "other",
377
- "test_field": test_field,
378
- "has_key": False,
379
- "priority": 3,
380
- "base_url": url
381
- })
382
- return {"success": True, "message": "Custom API added successfully"}
383
 
384
 
385
- # ------------------------------
386
- # Hugging Face synthetic endpoints
387
- # ------------------------------
388
- @app.get("/api/hf/health")
389
- async def hf_health():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  return {
391
- "space_status": "running",
392
- "last_refresh": _utc_now().isoformat(),
393
- "hardware": "cpu-basic",
394
- "queued_jobs": random.randint(0, 2),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  }
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- @app.post("/api/hf/refresh")
399
- async def hf_refresh():
400
- # pretend we refreshed space metadata / cache
401
- return {"success": True, "refreshed_at": _utc_now().isoformat()}
 
 
 
 
 
 
 
 
 
 
 
402
 
 
 
 
 
 
 
 
 
 
 
403
 
404
- @app.get("/api/hf/registry")
405
- async def hf_registry(kind: str = Query("models")):
406
- if kind == "datasets":
407
- return [{"id": "imdb", "downloads": 12345}, {"id": "ag_news", "downloads": 54321}]
408
- return [{"id": "distilbert-base-uncased", "likes": 12000}, {"id": "bert-base-uncased", "likes": 25000}]
 
 
 
 
409
 
 
 
 
 
 
 
 
 
410
 
411
- @app.get("/api/hf/search")
412
- async def hf_search(q: str, kind: str = Query("models")):
413
- # return mock search results
 
414
  return {
415
- "query": q,
416
- "kind": kind,
417
- "results": [
418
- {"id": f"{kind}/{q}-result-1", "score": round(random.uniform(0.6, 0.99), 3)},
419
- {"id": f"{kind}/{q}-result-2", "score": round(random.uniform(0.6, 0.99), 3)},
420
- ],
421
  }
422
 
 
 
 
 
 
 
 
 
423
 
424
- @app.post("/api/hf/run-sentiment")
425
- async def hf_run_sentiment(text: str = Body(..., embed=True)):
426
- # fake analyser
427
- label = random.choice(["POSITIVE", "NEGATIVE", "NEUTRAL"])
428
- score = round(random.uniform(0.5, 0.99), 3)
429
- return {"label": label, "score": score, "text": text}
 
430
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
- # ------------------------------
433
- # WebSocket with richer event types
434
- # ------------------------------
435
- class LiveConnections:
436
- def __init__(self) -> None:
437
- self.active: List[WebSocket] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- async def connect(self, websocket: WebSocket) -> None:
440
- await websocket.accept()
441
- self.active.append(websocket)
 
 
 
 
 
 
442
 
443
- def disconnect(self, websocket: WebSocket) -> None:
444
- if websocket in self.active:
445
- self.active.remove(websocket)
446
 
447
- async def broadcast(self, message: Dict[str, Any]) -> None:
448
- for ws in list(self.active):
449
- try:
450
- await ws.send_json(message)
451
- except Exception:
452
- self.disconnect(ws)
453
 
 
 
 
 
454
 
455
- live_manager = LiveConnections()
 
 
 
 
 
 
 
 
 
 
 
456
 
457
 
458
- @app.websocket("/ws/live")
459
- async def websocket_live(websocket: WebSocket):
460
- await live_manager.connect(websocket)
461
- try:
462
- await websocket.send_json({
463
- "type": "welcome",
464
- "service": "crypto-data-source",
465
- "timestamp": _utc_now().isoformat(),
 
466
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
- while True:
469
- # rotate between event types to satisfy different UIs
470
- event_choice = random.choice(["live_metrics", "status_update", "provider_status_change", "new_alert"])
471
- if event_choice == "live_metrics":
472
- payload = {
473
- "type": "live_metrics",
474
- "timestamp": _utc_now().isoformat(),
475
- "metrics": {
476
- "cpu_usage": random.randint(5, 40),
477
- "memory_usage": random.randint(20, 75),
478
- "active_requests": random.randint(1, 30),
479
- "error_rate_percent": round(random.uniform(0.0, 3.0), 2),
480
- },
481
- }
482
- elif event_choice == "status_update":
483
- payload = {"type": "status_update", "status": _status_payload()}
484
- elif event_choice == "provider_status_change":
485
- prov = random.choice(_generate_providers_snapshot())
486
- payload = {"type": "provider_status_change", "provider": prov}
487
- else:
488
- payload = {
489
- "type": "new_alert",
490
- "data": {
491
- "timestamp": _utc_now().isoformat(),
492
- "provider": random.choice(BASE_PROVIDERS)["name"],
493
- "error_type": random.choice(["HTTP 429", "HTTP 500", "Timeout"]),
494
- "message": "Synthetic alert for demo",
495
- },
496
- }
497
 
498
- await websocket.send_json(payload)
499
- await asyncio.sleep(4)
500
- except WebSocketDisconnect:
501
- live_manager.disconnect(websocket)
502
- except Exception:
503
- live_manager.disconnect(websocket)
504
-
505
-
506
- # ------------------------------
507
- # Serve root index.html if present
508
- # ------------------------------
509
- @app.get("/")
510
- async def root():
511
- if INDEX_FILE.exists():
512
- return FileResponse(INDEX_FILE)
513
- return JSONResponse(
514
- {"message": "index.html not found next to app.py.", "hint": "Put index.html in the same folder as app.py or use /docs."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  )
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
- @app.get("/{full_path:path}")
519
- async def spa_catch_all(full_path: str):
520
- if full_path.startswith("api/"):
521
- return JSONResponse({"error": "API endpoint not found", "path": full_path}, status_code=404)
522
- if INDEX_FILE.exists():
523
- return FileResponse(INDEX_FILE)
524
- return JSONResponse({"error": "No frontend available for this path.", "path": full_path}, status_code=404)
525
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
  if __name__ == "__main__":
528
- import uvicorn
529
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
6
+
7
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
8
+ from fastapi.responses import HTMLResponse, FileResponse, Response
9
+ from fastapi.staticfiles import StaticFiles
10
  from fastapi.middleware.cors import CORSMiddleware
11
+ from pydantic import BaseModel
12
+ from typing import List, Dict, Optional
 
13
  import asyncio
14
+ import aiohttp
15
  import random
16
+ import json
17
+ import logging
18
+ from datetime import datetime, timedelta
19
+ import uvicorn
20
+ from collections import defaultdict
21
+ import os
22
+ from urllib.parse import urljoin
23
+
24
+ from database import Database
25
+ from config import config as global_config
26
+ from starlette.middleware.trustedhost import TrustedHostMiddleware
27
+
28
+ class SentimentRequest(BaseModel):
29
+ texts: List[str]
30
+
31
+ class PoolCreate(BaseModel):
32
+ name: str
33
+ category: str
34
+ rotation_strategy: str = "round_robin"
35
+ description: Optional[str] = None
36
+
37
+ class PoolMemberAdd(BaseModel):
38
+ provider_id: str
39
+ priority: int = 1
40
+ weight: int = 1
41
+
42
+ logger = logging.getLogger("crypto_monitor")
43
+
44
+
45
+ app = FastAPI(title="Crypto Monitor Ultimate", version="3.0.0")
46
+
47
+
48
+ def _split_env_list(value: Optional[str]) -> List[str]:
49
+ if not value:
50
+ return []
51
+ return [item.strip() for item in value.split(",") if item.strip()]
52
+
53
+
54
+ allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "")
55
+ allowed_origin_regex_env = os.getenv("ALLOWED_ORIGIN_REGEX")
56
+ allowed_origins = _split_env_list(allowed_origins_env)
57
+
58
+ cors_kwargs = {
59
+ "allow_methods": ["*"],
60
+ "allow_headers": ["*"],
61
+ "allow_credentials": True,
62
+ }
63
+
64
+ if allowed_origin_regex_env:
65
+ cors_kwargs["allow_origin_regex"] = allowed_origin_regex_env
66
+ elif not allowed_origins or "*" in allowed_origins:
67
+ cors_kwargs["allow_origin_regex"] = ".*"
68
+ else:
69
+ cors_kwargs["allow_origins"] = allowed_origins
70
+
71
+ app.add_middleware(CORSMiddleware, **cors_kwargs)
72
+
73
+ trusted_hosts = _split_env_list(os.getenv("TRUSTED_HOSTS"))
74
+ if not trusted_hosts:
75
+ trusted_hosts = ["*"]
76
+ app.add_middleware(TrustedHostMiddleware, allowed_hosts=trusted_hosts)
77
 
78
+ # WebSocket Manager
79
+ class ConnectionManager:
80
+ def __init__(self):
81
+ self.active_connections: List[WebSocket] = []
82
 
83
+ async def connect(self, websocket: WebSocket):
84
+ await websocket.accept()
85
+ self.active_connections.append(websocket)
86
+
87
+ def disconnect(self, websocket: WebSocket):
88
+ self.active_connections.remove(websocket)
89
+
90
+ async def broadcast(self, message: dict):
91
+ for connection in self.active_connections:
92
+ try:
93
+ await connection.send_json(message)
94
+ except:
95
+ pass
96
+
97
+ manager = ConnectionManager()
98
+
99
+ db = Database("data/crypto_monitor.db")
100
+
101
+ # API Provider Configuration - Real Free APIs
102
+ API_PROVIDERS = {
103
+ "market_data": [
104
+ {
105
+ "name": "CoinGecko",
106
+ "base_url": "https://api.coingecko.com/api/v3",
107
+ "endpoints": {
108
+ "coins_markets": "/coins/markets",
109
+ "simple_price": "/simple/price",
110
+ "global": "/global",
111
+ "trending": "/search/trending"
112
+ },
113
+ "auth": None,
114
+ "rate_limit": "50/min",
115
+ "status": "active"
116
+ },
117
+ {
118
+ "name": "CoinCap",
119
+ "base_url": "https://api.coincap.io/v2",
120
+ "endpoints": {
121
+ "assets": "/assets",
122
+ "rates": "/rates"
123
+ },
124
+ "auth": None,
125
+ "rate_limit": "200/min",
126
+ "status": "active"
127
+ },
128
+ {
129
+ "name": "CoinStats",
130
+ "base_url": "https://api.coinstats.app",
131
+ "endpoints": {
132
+ "coins": "/public/v1/coins",
133
+ "charts": "/public/v1/charts"
134
+ },
135
+ "auth": None,
136
+ "rate_limit": "unlimited",
137
+ "status": "active"
138
+ },
139
+ {
140
+ "name": "Cryptorank",
141
+ "base_url": "https://api.cryptorank.io/v1",
142
+ "endpoints": {
143
+ "currencies": "/currencies"
144
+ },
145
+ "auth": None,
146
+ "rate_limit": "100/min",
147
+ "status": "active"
148
+ }
149
+ ],
150
+ "exchanges": [
151
+ {
152
+ "name": "Binance",
153
+ "base_url": "https://api.binance.com/api/v3",
154
+ "endpoints": {
155
+ "ticker": "/ticker/24hr",
156
+ "price": "/ticker/price"
157
+ },
158
+ "auth": None,
159
+ "rate_limit": "1200/min",
160
+ "status": "active"
161
+ },
162
+ {
163
+ "name": "Coinbase",
164
+ "base_url": "https://api.coinbase.com/v2",
165
+ "endpoints": {
166
+ "prices": "/prices",
167
+ "exchange_rates": "/exchange-rates"
168
+ },
169
+ "auth": None,
170
+ "rate_limit": "10000/hour",
171
+ "status": "active"
172
+ },
173
+ {
174
+ "name": "Kraken",
175
+ "base_url": "https://api.kraken.com/0/public",
176
+ "endpoints": {
177
+ "ticker": "/Ticker",
178
+ "trades": "/Trades"
179
+ },
180
+ "auth": None,
181
+ "rate_limit": "1/sec",
182
+ "status": "active"
183
+ }
184
+ ],
185
+ "news": [
186
+ {
187
+ "name": "CoinStats News",
188
+ "base_url": "https://api.coinstats.app",
189
+ "endpoints": {
190
+ "feed": "/public/v1/news"
191
+ },
192
+ "auth": None,
193
+ "rate_limit": "unlimited",
194
+ "status": "active"
195
+ },
196
+ {
197
+ "name": "CoinDesk RSS",
198
+ "base_url": "https://www.coindesk.com",
199
+ "endpoints": {
200
+ "rss": "/arc/outboundfeeds/rss/?outputType=xml"
201
+ },
202
+ "auth": None,
203
+ "rate_limit": "unlimited",
204
+ "status": "active"
205
+ },
206
+ {
207
+ "name": "Cointelegraph RSS",
208
+ "base_url": "https://cointelegraph.com",
209
+ "endpoints": {
210
+ "rss": "/rss"
211
+ },
212
+ "auth": None,
213
+ "rate_limit": "unlimited",
214
+ "status": "active"
215
+ }
216
+ ],
217
+ "sentiment": [
218
+ {
219
+ "name": "Alternative.me Fear & Greed",
220
+ "base_url": "https://api.alternative.me",
221
+ "endpoints": {
222
+ "fng": "/fng/?limit=1&format=json"
223
+ },
224
+ "auth": None,
225
+ "rate_limit": "unlimited",
226
+ "status": "active"
227
+ }
228
+ ],
229
+ "defi": [
230
+ {
231
+ "name": "DeFi Llama",
232
+ "base_url": "https://api.llama.fi",
233
+ "endpoints": {
234
+ "protocols": "/protocols",
235
+ "tvl": "/tvl"
236
+ },
237
+ "auth": None,
238
+ "rate_limit": "unlimited",
239
+ "status": "active"
240
+ },
241
+ {
242
+ "name": "1inch",
243
+ "base_url": "https://api.1inch.io/v5.0/1",
244
+ "endpoints": {
245
+ "quote": "/quote"
246
+ },
247
+ "auth": None,
248
+ "rate_limit": "unlimited",
249
+ "status": "active"
250
+ }
251
+ ],
252
+ "blockchain": [
253
+ {
254
+ "name": "Blockscout Ethereum",
255
+ "base_url": "https://eth.blockscout.com/api",
256
+ "endpoints": {
257
+ "balance": "/v2/addresses"
258
+ },
259
+ "auth": None,
260
+ "rate_limit": "unlimited",
261
+ "status": "active"
262
+ },
263
+ {
264
+ "name": "Ethplorer",
265
+ "base_url": "https://api.ethplorer.io",
266
+ "endpoints": {
267
+ "address": "/getAddressInfo"
268
+ },
269
+ "auth": {"type": "query", "key": "freekey"},
270
+ "rate_limit": "limited",
271
+ "status": "active"
272
+ }
273
+ ]
274
+ }
275
+
276
+ # Fallback data used when upstream APIs یا پایگاه داده در دسترس نیستند
277
+ DEFI_FALLBACK = [
278
+ {
279
+ "name": "Sample Protocol",
280
+ "tvl": 0.0,
281
+ "change_24h": 0.0,
282
+ "chain": "N/A",
283
+ }
284
  ]
285
 
286
+ # Health check configuration
287
+ HEALTH_TESTS = {
288
+ "CoinGecko": {"path": "/ping"},
289
+ "CoinCap": {"path": "/assets/bitcoin", "params": {"limit": 1}},
290
+ "CoinStats": {"path": "/public/v1/coins", "params": {"skip": 0, "limit": 1}},
291
+ "CoinStats News": {"path": "/public/v1/news", "params": {"skip": 0, "limit": 1}},
292
+ "Cryptorank": {"path": "/currencies"},
293
+ "Binance": {"path": "/ping"},
294
+ "Coinbase": {"path": "/exchange-rates"},
295
+ "Kraken": {"path": "/SystemStatus"},
296
+ "Alternative.me Fear & Greed": {"path": "/fng/", "params": {"limit": 1, "format": "json"}},
297
+ "DeFi Llama": {"path": "/protocols"},
298
+ "1inch": {"path": "/tokens"},
299
+ "Blockscout Ethereum": {"path": "/v2/stats"},
300
+ "Ethplorer": {"path": "/getTop", "params": {"apikey": "freekey"}},
301
+ "CoinDesk RSS": {"path": "/arc/outboundfeeds/rss/?outputType=xml"},
302
+ "Cointelegraph RSS": {"path": "/rss"}
303
+ }
304
+
305
+ KEY_HEADER_MAP = {
306
+ "CoinMarketCap": ("X-CMC_PRO_API_KEY", "plain"),
307
+ "CryptoCompare": ("Authorization", "apikey")
308
+ }
309
+
310
+ KEY_QUERY_MAP = {
311
+ "Etherscan": "apikey",
312
+ "BscScan": "apikey",
313
+ "TronScan": "apikey"
314
+ }
315
+
316
+ HEALTH_CACHE_TTL = 120 # seconds
317
+ provider_health_cache: Dict[str, Dict] = {}
318
+
319
+
320
+ def provider_slug(name: str) -> str:
321
+ return name.lower().replace(" ", "_")
322
+
323
+
324
+ def assemble_providers() -> List[Dict]:
325
+ providers: List[Dict] = []
326
+ seen = set()
327
+
328
+ for category, provider_list in API_PROVIDERS.items():
329
+ for provider in provider_list:
330
+ entry = {
331
+ "name": provider["name"],
332
+ "category": category,
333
+ "base_url": provider["base_url"],
334
+ "endpoints": provider.get("endpoints", {}),
335
+ "health_endpoint": provider.get("health_endpoint"),
336
+ "requires_key": False,
337
+ "api_key": None,
338
+ "timeout_ms": 10000
339
+ }
340
+
341
+ cfg = global_config.get_provider(provider["name"])
342
+ if cfg:
343
+ entry["health_endpoint"] = cfg.health_check_endpoint
344
+ entry["requires_key"] = cfg.requires_key
345
+ entry["api_key"] = cfg.api_key
346
+ entry["timeout_ms"] = cfg.timeout_ms
347
+
348
+ providers.append(entry)
349
+ seen.add(provider_slug(provider["name"]))
350
+
351
+ for cfg in global_config.get_all_providers():
352
+ slug = provider_slug(cfg.name)
353
+ if slug in seen:
354
+ continue
355
+
356
+ providers.append({
357
+ "name": cfg.name,
358
+ "category": cfg.category,
359
+ "base_url": cfg.endpoint_url,
360
+ "endpoints": {},
361
+ "health_endpoint": cfg.health_check_endpoint,
362
+ "requires_key": cfg.requires_key,
363
+ "api_key": cfg.api_key,
364
+ "timeout_ms": cfg.timeout_ms
365
+ })
366
 
367
+ return providers
368
+
369
+ # Cache for API responses
370
+ cache = {
371
+ "market_data": {"data": None, "timestamp": None, "ttl": 60},
372
+ "news": {"data": None, "timestamp": None, "ttl": 300},
373
+ "sentiment": {"data": None, "timestamp": None, "ttl": 3600},
374
+ "defi": {"data": None, "timestamp": None, "ttl": 300}
375
+ }
376
+
377
+ async def fetch_with_retry(session, url, retries=3):
378
+ """Fetch data with retry mechanism"""
379
+ for attempt in range(retries):
380
+ try:
381
+ async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
382
+ if response.status == 200:
383
+ return await response.json()
384
+ elif response.status == 429: # Rate limit
385
+ await asyncio.sleep(2 ** attempt)
386
+ else:
387
+ return None
388
+ except Exception as e:
389
+ if attempt == retries - 1:
390
+ print(f"Error fetching {url}: {e}")
391
+ return None
392
+ await asyncio.sleep(1)
393
+ return None
394
+
395
+ def is_cache_valid(cache_entry):
396
+ """Check if cache is still valid"""
397
+ if cache_entry["data"] is None or cache_entry["timestamp"] is None:
398
+ return False
399
+ elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
400
+ return elapsed < cache_entry["ttl"]
401
+
402
+ async def get_market_data():
403
+ """Fetch real market data from multiple sources"""
404
+ if is_cache_valid(cache["market_data"]):
405
+ return cache["market_data"]["data"]
406
+
407
+ async with aiohttp.ClientSession() as session:
408
+ # Try CoinGecko first
409
+ url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1"
410
+ data = await fetch_with_retry(session, url)
411
+
412
+ if data:
413
+ formatted_data = []
414
+ for coin in data[:20]:
415
+ formatted_data.append({
416
+ "symbol": coin.get("symbol", "").upper(),
417
+ "name": coin.get("name", ""),
418
+ "price": coin.get("current_price", 0),
419
+ "change_24h": coin.get("price_change_percentage_24h", 0),
420
+ "market_cap": coin.get("market_cap", 0),
421
+ "volume_24h": coin.get("total_volume", 0),
422
+ "rank": coin.get("market_cap_rank", 0),
423
+ "image": coin.get("image", "")
424
+ })
425
+
426
+ cache["market_data"]["data"] = formatted_data
427
+ cache["market_data"]["timestamp"] = datetime.now()
428
+ return formatted_data
429
+
430
+ # Fallback to CoinCap
431
+ url = "https://api.coincap.io/v2/assets?limit=20"
432
+ data = await fetch_with_retry(session, url)
433
+
434
+ if data and "data" in data:
435
+ formatted_data = []
436
+ for coin in data["data"]:
437
+ formatted_data.append({
438
+ "symbol": coin.get("symbol", "").upper(),
439
+ "name": coin.get("name", ""),
440
+ "price": float(coin.get("priceUsd", 0)),
441
+ "change_24h": float(coin.get("changePercent24Hr", 0)),
442
+ "market_cap": float(coin.get("marketCapUsd", 0)),
443
+ "volume_24h": float(coin.get("volumeUsd24Hr", 0)),
444
+ "rank": int(coin.get("rank", 0)),
445
+ "image": ""
446
+ })
447
+
448
+ cache["market_data"]["data"] = formatted_data
449
+ cache["market_data"]["timestamp"] = datetime.now()
450
+ return formatted_data
451
+
452
+ return []
453
+
454
+ async def get_global_stats():
455
+ """Fetch global crypto market statistics"""
456
+ async with aiohttp.ClientSession() as session:
457
+ # CoinGecko global data
458
+ url = "https://api.coingecko.com/api/v3/global"
459
+ data = await fetch_with_retry(session, url)
460
+
461
+ if data and "data" in data:
462
+ global_data = data["data"]
463
+ return {
464
+ "total_market_cap": global_data.get("total_market_cap", {}).get("usd", 0),
465
+ "total_volume": global_data.get("total_volume", {}).get("usd", 0),
466
+ "btc_dominance": global_data.get("market_cap_percentage", {}).get("btc", 0),
467
+ "eth_dominance": global_data.get("market_cap_percentage", {}).get("eth", 0),
468
+ "active_cryptocurrencies": global_data.get("active_cryptocurrencies", 0),
469
+ "markets": global_data.get("markets", 0)
470
+ }
471
+
472
+ return {
473
+ "total_market_cap": 0,
474
+ "total_volume": 0,
475
+ "btc_dominance": 0,
476
+ "eth_dominance": 0,
477
+ "active_cryptocurrencies": 0,
478
+ "markets": 0
479
+ }
480
 
481
+ async def get_trending():
482
+ """Fetch trending coins"""
483
+ async with aiohttp.ClientSession() as session:
484
+ url = "https://api.coingecko.com/api/v3/search/trending"
485
+ data = await fetch_with_retry(session, url)
486
+
487
+ if data and "coins" in data:
488
+ return [
489
+ {
490
+ "name": coin["item"].get("name", ""),
491
+ "symbol": coin["item"].get("symbol", "").upper(),
492
+ "rank": coin["item"].get("market_cap_rank", 0),
493
+ "thumb": coin["item"].get("thumb", "")
494
+ }
495
+ for coin in data["coins"][:7]
496
+ ]
497
+
498
+ return []
499
+
500
+ async def get_sentiment():
501
+ """Fetch Fear & Greed Index"""
502
+ if is_cache_valid(cache["sentiment"]):
503
+ return cache["sentiment"]["data"]
504
+
505
+ async with aiohttp.ClientSession() as session:
506
+ url = "https://api.alternative.me/fng/?limit=1&format=json"
507
+ data = await fetch_with_retry(session, url)
508
+
509
+ if data and "data" in data and len(data["data"]) > 0:
510
+ fng_data = data["data"][0]
511
+ result = {
512
+ "value": int(fng_data.get("value", 50)),
513
+ "classification": fng_data.get("value_classification", "Neutral"),
514
+ "timestamp": fng_data.get("timestamp", "")
515
+ }
516
+ cache["sentiment"]["data"] = result
517
+ cache["sentiment"]["timestamp"] = datetime.now()
518
+ return result
519
+
520
+ return {"value": 50, "classification": "Neutral", "timestamp": ""}
521
+
522
+ async def get_defi_tvl():
523
+ """Fetch DeFi Total Value Locked"""
524
+ if is_cache_valid(cache["defi"]):
525
+ return cache["defi"]["data"]
526
+
527
+ async with aiohttp.ClientSession() as session:
528
+ url = "https://api.llama.fi/protocols"
529
+ data = await fetch_with_retry(session, url)
530
+
531
+ if data and isinstance(data, list):
532
+ top_protocols = sorted(data, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
533
+ result = [
534
+ {
535
+ "name": p.get("name", ""),
536
+ "tvl": p.get("tvl", 0),
537
+ "change_24h": p.get("change_1d", 0),
538
+ "chain": p.get("chain", "")
539
+ }
540
+ for p in top_protocols
541
+ ]
542
+ cache["defi"]["data"] = result
543
+ cache["defi"]["timestamp"] = datetime.now()
544
+ return result
545
+
546
+ return []
547
+
548
+ async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict, force_refresh: bool = False) -> Dict:
549
+ """Fetch real health information for a provider"""
550
+ name = provider["name"]
551
+ cached = provider_health_cache.get(name)
552
+ if cached and not force_refresh:
553
+ age = (datetime.now() - cached["timestamp"]).total_seconds()
554
+ if age < HEALTH_CACHE_TTL:
555
+ return cached["data"]
556
+
557
+ health_config = HEALTH_TESTS.get(name, {})
558
+ health_endpoint = provider.get("health_endpoint") or health_config.get("path")
559
+ if not health_endpoint:
560
+ endpoints = provider.get("endpoints", {})
561
+ health_endpoint = next(iter(endpoints.values()), "/")
562
+
563
+ params = dict(health_config.get("params", {}))
564
+ headers = {
565
+ "User-Agent": "CryptoMonitor/1.0 (+https://github.com/nimazasinich/crypto-dt-source)"
566
+ }
567
 
568
+ requires_key = provider.get("requires_key", False)
569
+ api_key = provider.get("api_key")
570
+ cfg = global_config.get_provider(name)
571
+ if cfg:
572
+ requires_key = cfg.requires_key
573
+ if not api_key:
574
+ api_key = cfg.api_key
575
 
576
+ if health_endpoint.startswith("http"):
577
+ url = health_endpoint
 
 
578
  else:
579
+ url = urljoin(provider["base_url"].rstrip("/") + "/", health_endpoint.lstrip("/"))
580
+
581
+ if requires_key:
582
+ if not api_key:
583
+ result = {
584
+ "name": name,
585
+ "category": provider["category"],
586
+ "base_url": provider["base_url"],
587
+ "status": "degraded",
588
+ "uptime": db.get_uptime_percentage(name),
589
+ "response_time_ms": None,
590
+ "rate_limit": "",
591
+ "endpoints": len(provider.get("endpoints", {})),
592
+ "last_fetch": datetime.now().isoformat(),
593
+ "last_check": datetime.now().isoformat(),
594
+ "message": "API key not configured"
595
+ }
596
+ provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
597
+ db.log_provider_status(name, provider["category"], "degraded", endpoint_tested=url, error_message="missing_api_key")
598
+ return result
599
+
600
+ header_mapping = KEY_HEADER_MAP.get(name)
601
+ if header_mapping:
602
+ header_name, mode = header_mapping
603
+ if mode == "plain":
604
+ headers[header_name] = api_key
605
+ elif mode == "apikey":
606
+ headers[header_name] = f"Apikey {api_key}"
607
+ else:
608
+ query_key = KEY_QUERY_MAP.get(name)
609
+ if query_key:
610
+ params[query_key] = api_key
611
+ else:
612
+ headers["Authorization"] = f"Bearer {api_key}"
613
+
614
+ timeout_total = max(provider.get("timeout_ms", 10000) / 1000, 5)
615
+ timeout = aiohttp.ClientTimeout(total=timeout_total)
616
+ loop = asyncio.get_running_loop()
617
+ start_time = loop.time()
618
+
619
+ status = "offline"
620
+ status_code = None
621
+ error_message = None
622
+ response_time_ms = None
623
+
624
+ try:
625
+ async with session.get(url, params=params, headers=headers, timeout=timeout) as response:
626
+ status_code = response.status
627
+ response_time_ms = round((loop.time() - start_time) * 1000, 2)
628
+
629
+ if status_code < 400:
630
+ status = "online"
631
+ elif status_code < 500:
632
+ status = "degraded"
633
+ else:
634
+ status = "offline"
635
+
636
+ if status != "online":
637
+ try:
638
+ error_message = await response.text()
639
+ except Exception:
640
+ error_message = f"HTTP {status_code}"
641
+ except Exception as exc:
642
  status = "offline"
643
+ error_message = str(exc)
644
+
645
+ db.log_provider_status(
646
+ name,
647
+ provider["category"],
648
+ status,
649
+ response_time=response_time_ms,
650
+ status_code=status_code,
651
+ endpoint_tested=url,
652
+ error_message=error_message[:500] if error_message else None
653
+ )
654
+
655
+ uptime = db.get_uptime_percentage(name)
656
+ avg_response = db.get_avg_response_time(name)
657
 
658
+ result = {
659
+ "name": name,
 
660
  "category": provider["category"],
661
+ "base_url": provider["base_url"],
662
  "status": status,
663
+ "uptime": uptime,
664
+ "response_time_ms": response_time_ms,
665
+ "avg_response_time_ms": avg_response,
666
+ "rate_limit": provider.get("rate_limit", ""),
667
+ "endpoints": len(provider.get("endpoints", {})),
668
+ "last_fetch": datetime.now().isoformat(),
669
+ "last_check": datetime.now().isoformat(),
670
+ "status_code": status_code,
671
+ "message": error_message[:200] if error_message else None
672
  }
 
 
 
 
 
 
 
 
 
 
673
 
674
+ provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
675
+ return result
676
 
677
+
678
+ async def get_provider_stats(force_refresh: bool = False):
679
+ """Generate provider statistics with real health checks"""
680
+ providers = assemble_providers()
681
+ async with aiohttp.ClientSession() as session:
682
+ results = await asyncio.gather(
683
+ *(fetch_provider_health(session, provider, force_refresh) for provider in providers)
684
+ )
685
+ return results
686
+
687
+ # API Endpoints
688
+
689
+ @app.get("/api/info")
690
+ async def api_info():
691
+ total_providers = sum(len(providers) for providers in API_PROVIDERS.values())
692
+ return {
693
+ "name": "Crypto Monitor Ultimate",
694
+ "version": "3.0.0",
695
+ "description": "Real-time crypto monitoring with 100+ free APIs",
696
+ "total_providers": total_providers,
697
+ "categories": list(API_PROVIDERS.keys()),
698
+ "features": [
699
+ "Real market data from CoinGecko, CoinCap",
700
+ "Live exchange data from Binance, Coinbase, Kraken",
701
+ "Crypto news aggregation",
702
+ "Fear & Greed Index sentiment",
703
+ "DeFi TVL tracking",
704
+ "Blockchain explorer integration",
705
+ "Real-time WebSocket updates"
706
+ ]
707
+ }
708
+
709
+ @app.get("/health")
710
+ async def health():
711
+ providers = await get_provider_stats()
712
+ total = len(providers)
713
+ online = len([p for p in providers if p["status"] == "online"])
714
+ degraded = len([p for p in providers if p["status"] == "degraded"])
715
+
716
+ categories: Dict[str, int] = defaultdict(int)
717
+ for provider in providers:
718
+ categories[provider["category"]] += 1
719
+
720
+ return {
721
+ "status": "healthy" if total == 0 or online >= total * 0.8 else "degraded",
722
+ "timestamp": datetime.now().isoformat(),
723
+ "providers": {
724
+ "total": total,
725
+ "operational": online,
726
+ "degraded": degraded,
727
+ "offline": total - online - degraded
728
+ },
729
+ "categories": dict(categories)
730
+ }
731
 
732
 
 
 
 
733
  @app.get("/api/health")
734
  async def api_health():
735
+ """Compatibility endpoint mirroring /health"""
736
+ return await health()
737
+
738
+ @app.get("/api/market")
739
+ async def market():
740
+ """Get real-time market data"""
741
+ data = await get_market_data()
742
+ global_stats = await get_global_stats()
743
+
744
+ return {
745
+ "cryptocurrencies": data,
746
+ "global": global_stats,
747
+ "timestamp": datetime.now().isoformat(),
748
+ "source": "CoinGecko/CoinCap"
749
+ }
750
+
751
+ @app.get("/api/trending")
752
+ async def trending():
753
+ """Get trending coins"""
754
+ data = await get_trending()
755
  return {
756
+ "trending": data,
757
+ "timestamp": datetime.now().isoformat(),
758
+ "source": "CoinGecko"
 
759
  }
760
 
761
+ @app.get("/api/sentiment")
762
+ async def sentiment():
763
+ """Get Fear & Greed Index"""
764
+ data = await get_sentiment()
765
+ return {
766
+ "fear_greed_index": data,
767
+ "timestamp": datetime.now().isoformat(),
768
+ "source": "Alternative.me"
769
+ }
770
 
771
+ @app.get("/api/defi")
772
+ async def defi():
773
+ """Get DeFi protocols and TVL"""
774
  try:
775
+ data = await get_defi_tvl()
776
+ except Exception as exc: # pragma: no cover - defensive
777
+ logger.warning("defi endpoint fallback due to error: %s", exc)
778
+ data = []
 
 
 
 
 
779
 
780
+ if not data:
781
+ data = DEFI_FALLBACK
782
 
783
+ total_tvl = sum(p.get("tvl", 0) for p in data)
784
+ return {
785
+ "protocols": data,
786
+ "total_tvl": total_tvl,
787
+ "timestamp": datetime.now().isoformat(),
788
+ "source": "DeFi Llama (fallback)" if data == DEFI_FALLBACK else "DeFi Llama"
789
+ }
790
+
791
+ @app.get("/api/providers")
792
+ async def providers():
793
+ """Get all API providers status"""
794
+ data = await get_provider_stats()
795
+ return data
 
 
 
 
 
 
 
 
 
 
 
 
796
 
797
+ @app.get("/api/status")
798
+ async def status():
799
+ """Get system status for dashboard"""
800
+ providers = await get_provider_stats()
801
+ online = len([p for p in providers if p.get("status") == "online"])
802
+ offline = len([p for p in providers if p.get("status") == "offline"])
803
+ degraded = len([p for p in providers if p.get("status") == "degraded"])
804
+ avg_response = 0.0
805
+ if providers:
806
+ response_values = [
807
+ p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
808
+ for p in providers
809
+ ]
810
+ avg_response = sum(response_values) / len(response_values)
811
+
812
  return {
813
+ "total_providers": len(providers),
 
 
 
 
 
814
  "online": online,
815
  "offline": offline,
816
  "degraded": degraded,
817
+ "avg_response_time_ms": round(avg_response, 1),
818
+ "system_health": "healthy" if not providers or online >= len(providers) * 0.8 else "degraded",
819
+ "timestamp": datetime.now().isoformat()
 
820
  }
821
 
822
 
823
+ @app.get("/status", include_in_schema=False)
824
+ async def status_legacy():
825
+ return await status()
826
 
827
 
828
+ @app.get("/info", include_in_schema=False)
829
+ async def info_legacy():
830
+ return await api_info()
 
 
831
 
832
 
833
+ @app.get("/system/info", include_in_schema=False)
834
+ async def system_info():
835
+ return await api_info()
 
836
 
837
+ @app.get("/api/stats")
838
+ async def stats():
839
+ """Get comprehensive statistics"""
840
+ market = await get_market_data()
841
+ global_stats = await get_global_stats()
842
+ providers = await get_provider_stats()
843
+ sentiment_data = await get_sentiment()
844
+
845
+ return {
846
+ "market": {
847
+ "total_market_cap": global_stats["total_market_cap"],
848
+ "total_volume": global_stats["total_volume"],
849
+ "btc_dominance": global_stats["btc_dominance"],
850
+ "active_cryptos": global_stats["active_cryptocurrencies"],
851
+ "top_crypto_count": len(market)
852
+ },
853
+ "sentiment": {
854
+ "fear_greed_value": sentiment_data["value"],
855
+ "classification": sentiment_data["classification"]
856
+ },
857
+ "providers": {
858
+ "total": len(providers),
859
+ "operational": len([p for p in providers if p["status"] == "online"]),
860
+ "degraded": len([p for p in providers if p["status"] == "degraded"]),
861
+ "avg_uptime": round(sum(p.get("uptime", 0) for p in providers) / len(providers), 2) if providers else 0,
862
+ "avg_response_time": round(
863
+ sum((p.get("avg_response_time_ms") or p.get("response_time_ms") or 0) for p in providers) / len(providers),
864
+ 1
865
+ ) if providers else 0
866
+ },
867
+ "timestamp": datetime.now().isoformat()
868
+ }
869
+
870
+ # HuggingFace endpoints (mock for now)
871
+ @app.get("/api/hf/health")
872
+ async def hf_health():
873
+ return {
874
+ "status": "healthy",
875
+ "model_loaded": True,
876
+ "timestamp": datetime.now().isoformat()
877
+ }
878
+
879
+ @app.post("/api/hf/run-sentiment")
880
+ async def hf_run_sentiment(request: SentimentRequest):
881
+ """Run sentiment analysis on crypto text"""
882
+ texts = request.texts
883
+
884
+ # Mock sentiment analysis
885
+ # In production, this would call HuggingFace API
886
+ results = []
887
+ total_vote = 0
888
+
889
+ for text in texts:
890
+ # Simple mock sentiment
891
+ text_lower = text.lower()
892
+ positive_words = ["bullish", "strong", "breakout", "pump", "moon", "buy", "up"]
893
+ negative_words = ["bearish", "weak", "crash", "dump", "sell", "down", "drop"]
894
+
895
+ positive_score = sum(1 for word in positive_words if word in text_lower)
896
+ negative_score = sum(1 for word in negative_words if word in text_lower)
897
+
898
+ sentiment_score = (positive_score - negative_score) / max(len(text.split()), 1)
899
+ total_vote += sentiment_score
900
+
901
+ results.append({
902
+ "text": text,
903
+ "sentiment": "positive" if sentiment_score > 0 else "negative" if sentiment_score < 0 else "neutral",
904
+ "score": round(sentiment_score, 3)
905
+ })
906
+
907
+ avg_vote = total_vote / len(texts) if texts else 0
908
+
909
+ return {
910
+ "vote": round(avg_vote, 3),
911
+ "results": results,
912
+ "timestamp": datetime.now().isoformat()
913
+ }
914
+
915
+ @app.websocket("/ws/live")
916
+ async def websocket_endpoint(websocket: WebSocket):
917
+ """Real-time WebSocket updates"""
918
+ await manager.connect(websocket)
919
+ try:
920
+ while True:
921
+ await asyncio.sleep(5)
922
+
923
+ # Send market update
924
+ market_data = await get_market_data()
925
+ if market_data:
926
+ await websocket.send_json({
927
+ "type": "market_update",
928
+ "data": market_data[:5], # Top 5 coins
929
+ "timestamp": datetime.now().isoformat()
930
+ })
931
+
932
+ # Send sentiment update every 30 seconds
933
+ if random.random() > 0.8:
934
+ sentiment_data = await get_sentiment()
935
+ await websocket.send_json({
936
+ "type": "sentiment_update",
937
+ "data": sentiment_data,
938
+ "timestamp": datetime.now().isoformat()
939
+ })
940
+
941
+ except WebSocketDisconnect:
942
+ manager.disconnect(websocket)
943
+ except Exception as exc:
944
+ manager.disconnect(websocket)
945
+ logger.debug("WebSocket session ended: %s", exc)
946
 
 
 
 
 
 
 
947
 
948
+ @app.websocket("/api/ws/live")
949
+ async def websocket_endpoint_api(websocket: WebSocket):
950
+ await websocket_endpoint(websocket)
951
+
952
+ # Serve HTML files
953
+ @app.get("/", response_class=HTMLResponse)
954
+ async def root_html():
955
+ try:
956
+ with open("unified_dashboard.html", "r", encoding="utf-8") as f:
957
+ return HTMLResponse(content=f.read())
958
+ except:
959
+ try:
960
+ with open("index.html", "r", encoding="utf-8") as f:
961
+ return HTMLResponse(content=f.read())
962
+ except:
963
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
964
+
965
+ @app.get("/unified", response_class=HTMLResponse)
966
+ async def unified_dashboard():
967
+ try:
968
+ with open("unified_dashboard.html", "r", encoding="utf-8") as f:
969
+ return HTMLResponse(content=f.read())
970
+ except:
971
+ return HTMLResponse("<h1>Unified Dashboard not found</h1>", 404)
972
+
973
+ @app.get("/dashboard", response_class=HTMLResponse)
974
+ async def dashboard():
975
+ try:
976
+ with open("index.html", "r", encoding="utf-8") as f:
977
+ return HTMLResponse(content=f.read())
978
+ except:
979
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
980
+
981
+ @app.get("/dashboard.html", response_class=HTMLResponse)
982
+ async def dashboard_html():
983
+ try:
984
+ with open("dashboard.html", "r", encoding="utf-8") as f:
985
+ return HTMLResponse(content=f.read())
986
+ except:
987
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
988
+
989
+ @app.get("/enhanced_dashboard.html", response_class=HTMLResponse)
990
+ async def enhanced_dashboard():
991
+ try:
992
+ with open("enhanced_dashboard.html", "r", encoding="utf-8") as f:
993
+ return HTMLResponse(content=f.read())
994
+ except:
995
+ return HTMLResponse("<h1>Enhanced Dashboard not found</h1>", 404)
996
+
997
+ @app.get("/admin.html", response_class=HTMLResponse)
998
+ async def admin():
999
+ try:
1000
+ with open("admin.html", "r", encoding="utf-8") as f:
1001
+ return HTMLResponse(content=f.read())
1002
+ except:
1003
+ return HTMLResponse("<h1>Admin Panel not found</h1>", 404)
1004
+
1005
+ @app.get("/hf_console.html", response_class=HTMLResponse)
1006
+ async def hf_console():
1007
+ try:
1008
+ with open("hf_console.html", "r", encoding="utf-8") as f:
1009
+ return HTMLResponse(content=f.read())
1010
+ except:
1011
+ return HTMLResponse("<h1>HF Console not found</h1>", 404)
1012
+
1013
+ @app.get("/pool_management.html", response_class=HTMLResponse)
1014
+ async def pool_management():
1015
+ try:
1016
+ with open("pool_management.html", "r", encoding="utf-8") as f:
1017
+ return HTMLResponse(content=f.read())
1018
+ except:
1019
+ return HTMLResponse("<h1>Pool Management not found</h1>", 404)
1020
+
1021
+
1022
+
1023
+ # --- UI helper endpoints for categories, rate limits, logs, alerts, and HuggingFace registry ---
1024
 
1025
  @app.get("/api/categories")
1026
  async def api_categories():
1027
+ """Aggregate providers by category for the dashboard UI"""
1028
+ providers = await get_provider_stats()
1029
+ categories_map: Dict[str, Dict] = {}
1030
  for p in providers:
1031
+ cat = p.get("category", "uncategorized")
1032
+ entry = categories_map.setdefault(cat, {
1033
+ "name": cat,
1034
+ "total_sources": 0,
1035
+ "online": 0,
1036
+ "health_percentage": 0.0,
1037
+ "avg_response": 0.0,
1038
+ "last_updated": None,
1039
+ "status": "unknown",
1040
+ })
1041
+ entry["total_sources"] += 1
1042
+ if p.get("status") == "online":
1043
+ entry["online"] += 1
1044
+ resp = p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
1045
+ entry["avg_response"] += resp
1046
+ last_check = p.get("last_check") or p.get("last_fetch")
1047
+ if last_check:
1048
+ if not entry["last_updated"] or last_check > entry["last_updated"]:
1049
+ entry["last_updated"] = last_check
1050
+
1051
+ results = []
1052
+ for cat, entry in categories_map.items():
1053
+ total = max(entry["total_sources"], 1)
1054
+ online = entry["online"]
1055
+ health_pct = (online / total) * 100.0
1056
+ avg_response = entry["avg_response"] / total if entry["total_sources"] else 0.0
1057
+ if health_pct >= 80:
1058
+ status = "healthy"
1059
+ elif health_pct >= 50:
1060
+ status = "degraded"
1061
+ else:
1062
+ status = "critical"
1063
+ results.append({
1064
+ "name": entry["name"],
1065
+ "total_sources": total,
1066
+ "online": online,
1067
+ "health_percentage": round(health_pct, 2),
1068
+ "avg_response": round(avg_response, 1),
1069
+ "last_updated": entry["last_updated"] or datetime.now().isoformat(),
1070
+ "status": status,
1071
  })
1072
+ return results
1073
+
1074
+
1075
+ @app.get("/api/rate-limits")
1076
+ async def api_rate_limits():
1077
+ """Expose simple rate-limit information per provider for the UI cards"""
1078
+ providers = await get_provider_stats()
1079
+ now = datetime.now()
1080
+ items = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
  for p in providers:
1082
+ rate_str = p.get("rate_limit") or ""
1083
+ limit_val = 0
1084
+ window = "unknown"
1085
+ if rate_str and rate_str.lower() != "unlimited":
1086
+ parts = rate_str.split("/")
1087
+ try:
1088
+ limit_val = int("".join(ch for ch in parts[0] if ch.isdigit()))
1089
+ except ValueError:
1090
+ limit_val = 0
1091
+ if len(parts) > 1:
1092
+ window = parts[1]
1093
+ elif rate_str.lower() == "unlimited":
1094
+ limit_val = 0
1095
+ window = "unlimited"
1096
+
1097
+ status = p.get("status") or "unknown"
1098
+ if limit_val > 0:
1099
+ if status == "online":
1100
+ used = int(limit_val * 0.4)
1101
+ elif status == "degraded":
1102
+ used = int(limit_val * 0.7)
1103
+ else:
1104
+ used = int(limit_val * 0.1)
1105
+ else:
1106
+ used = 0
1107
+
1108
+ success_rate = p.get("uptime") or 0.0
1109
+ error_rate = max(0.0, 100.0 - success_rate)
1110
  items.append({
1111
+ "provider": p.get("name"),
1112
+ "category": p.get("category"),
1113
+ "plan": "free-tier",
1114
+ "used": used,
1115
+ "limit": limit_val,
1116
+ "window": window,
1117
+ "reset_time": (now + timedelta(minutes=15)).isoformat(),
1118
+ "success_rate": round(success_rate, 2),
1119
+ "error_rate": round(error_rate, 2),
1120
+ "avg_response": round(p.get("avg_response_time_ms") or 0.0, 1),
1121
+ "last_checked": p.get("last_check") or now.isoformat(),
1122
+ "notes": f"Status: {status}",
1123
  })
1124
  return items
1125
 
1126
 
1127
+ @app.get("/api/logs")
1128
+ async def api_logs(type: str = "all"):
1129
+ """Return recent connection logs from SQLite for the logs tab"""
1130
+ rows = db.get_recent_status(hours=24, limit=500)
1131
+ logs = []
1132
+ for row in rows:
1133
+ status = row.get("status") or "unknown"
1134
+ is_error = status != "online"
1135
+ if type == "errors" and not is_error:
1136
+ continue
1137
+ if type == "incidents" and not is_error:
1138
+ continue
1139
+ msg = row.get("error_message") or ""
1140
+ if not msg and row.get("status_code"):
1141
+ msg = f"HTTP {row['status_code']} on {row.get('endpoint_tested') or ''}".strip()
1142
  logs.append({
1143
+ "timestamp": row.get("timestamp") or row.get("created_at"),
1144
+ "provider": row.get("provider_name") or "System",
1145
+ "type": "error" if is_error else "info",
1146
+ "status": status,
1147
+ "response_time": row.get("response_time"),
1148
+ "message": msg or "No message",
 
 
1149
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
1150
  return logs
1151
 
1152
 
1153
+ @app.get("/api/alerts")
1154
+ async def api_alerts():
1155
+ """Expose active/unacknowledged alerts for the alerts tab"""
1156
+ try:
1157
+ rows = db.get_unacknowledged_alerts()
1158
+ except Exception:
1159
+ return []
1160
+ alerts = []
1161
+ for row in rows:
1162
+ severity = row.get("alert_type") or "warning"
1163
+ provider = row.get("provider_name") or "System"
1164
+ title = f"{severity.title()} alert - {provider}"
1165
+ alerts.append({
1166
+ "severity": severity.lower(),
1167
+ "title": title,
1168
+ "timestamp": row.get("triggered_at") or datetime.now().isoformat(),
1169
+ "message": row.get("message") or "",
1170
+ "provider": provider,
1171
+ })
1172
+ return alerts
1173
+
1174
+
1175
+
1176
+ HF_MODELS: List[Dict] = []
1177
+ HF_DATASETS: List[Dict] = []
1178
+ HF_CACHE_TS: Optional[datetime] = None
1179
+
1180
+
1181
+ async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit: int = 12) -> List[Dict]:
1182
  """
1183
+ Fetch a small registry snapshot from Hugging Face Hub.
1184
+ If the request fails for any reason, falls back to a small built-in sample.
1185
  """
1186
+ global HF_MODELS, HF_DATASETS, HF_CACHE_TS
1187
+
1188
+ # Basic in-memory TTL cache (6 hours)
1189
+ now = datetime.now()
1190
+ if HF_CACHE_TS and (now - HF_CACHE_TS).total_seconds() < 6 * 3600:
1191
+ if kind == "models" and HF_MODELS:
1192
+ return HF_MODELS
1193
+ if kind == "datasets" and HF_DATASETS:
1194
+ return HF_DATASETS
1195
+
1196
+ base_url = "https://huggingface.co/api/models" if kind == "models" else "https://huggingface.co/api/datasets"
1197
+ params = {"search": query, "limit": str(limit)}
1198
+ headers: Dict[str, str] = {}
1199
+ token = os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN")
1200
+ if token:
1201
+ headers["Authorization"] = f"Bearer {token}"
1202
+
1203
+ items: List[Dict] = []
1204
+ try:
1205
+ async with aiohttp.ClientSession() as session:
1206
+ async with session.get(base_url, params=params, headers=headers, timeout=10) as resp:
1207
+ if resp.status == 200:
1208
+ raw = await resp.json()
1209
+ # HF returns a list of models/datasets
1210
+ for entry in raw:
1211
+ item = {
1212
+ "id": entry.get("id") or entry.get("name"),
1213
+ "description": entry.get("pipeline_tag")
1214
+ or entry.get("cardData", {}).get("summary")
1215
+ or entry.get("description", ""),
1216
+ "downloads": entry.get("downloads", 0),
1217
+ "likes": entry.get("likes", 0),
1218
+ }
1219
+ items.append(item)
1220
+ except Exception:
1221
+ # ignore and fall back
1222
+ items = []
1223
+
1224
+ # Fallback sample if nothing was fetched
1225
+ if not items:
1226
+ if kind == "models":
1227
+ items = [
1228
+ {
1229
+ "id": "distilbert-base-uncased-finetuned-sst-2-english",
1230
+ "description": "English sentiment analysis model (SST-2).",
1231
+ "downloads": 100000,
1232
+ "likes": 1200,
1233
+ },
1234
+ {
1235
+ "id": "bert-base-multilingual-cased",
1236
+ "description": "Multilingual BERT model suitable for many languages.",
1237
+ "downloads": 500000,
1238
+ "likes": 4000,
1239
+ },
1240
+ ]
1241
+ else:
1242
+ items = [
1243
+ {
1244
+ "id": "crypto-sentiment-demo",
1245
+ "description": "Synthetic crypto sentiment dataset for demo purposes.",
1246
+ "downloads": 1200,
1247
+ "likes": 40,
1248
+ },
1249
+ {
1250
+ "id": "financial-news-sample",
1251
+ "description": "Sample of financial news headlines.",
1252
+ "downloads": 800,
1253
+ "likes": 25,
1254
+ },
1255
+ ]
1256
+
1257
+ # Update cache
1258
+ if kind == "models":
1259
+ HF_MODELS = items
1260
+ else:
1261
+ HF_DATASETS = items
1262
+ HF_CACHE_TS = now
1263
+ return items
1264
 
1265
 
1266
+ @app.post("/api/hf/refresh")
1267
+ async def hf_refresh():
1268
+ """Refresh HF registry data used by the UI."""
1269
+ models = await _fetch_hf_registry("models")
1270
+ datasets = await _fetch_hf_registry("datasets")
1271
+ return {"status": "ok", "models": len(models), "datasets": len(datasets)}
 
 
 
 
 
 
 
 
1272
 
1273
 
1274
+ @app.get("/api/hf/registry")
1275
+ async def hf_registry(type: str = "models"):
1276
+ """Return model/dataset registry for the HF panel."""
1277
+ if type == "datasets":
1278
+ data = await _fetch_hf_registry("datasets")
1279
+ else:
1280
+ data = await _fetch_hf_registry("models")
1281
+ return data
1282
 
1283
 
1284
+ @app.get("/api/hf/search")
1285
+ async def hf_search(q: str = "", kind: str = "models"):
1286
+ """Search over the HF registry."""
1287
+ pool = await _fetch_hf_registry("models" if kind == "models" else "datasets")
1288
+ q_lower = (q or "").lower()
1289
+ results: List[Dict] = []
1290
+ for item in pool:
1291
+ text = f"{item.get('id','')} {item.get('description','')}".lower()
1292
+ if not q_lower or q_lower in text:
1293
+ results.append(item)
1294
+ return results
1295
+
1296
+
1297
+ @app.get("/providers", include_in_schema=False)
1298
+ async def providers_legacy():
1299
+ return await providers()
1300
+
1301
+
1302
+ @app.get("/providers/health", include_in_schema=False)
1303
+ async def providers_health_legacy():
1304
+ data = await providers()
1305
+ total = len(data)
1306
+ online = len([p for p in data if p.get("status") == "online"])
1307
+ degraded = len([p for p in data if p.get("status") == "degraded"])
1308
+ return {
1309
+ "providers": data,
1310
+ "summary": {
1311
+ "total": total,
1312
+ "online": online,
1313
+ "degraded": degraded,
1314
+ "offline": total - online - degraded,
1315
+ },
1316
+ "timestamp": datetime.now().isoformat(),
1317
+ }
1318
 
1319
 
1320
+ @app.get("/categories", include_in_schema=False)
1321
+ async def categories_legacy():
1322
+ return await api_categories()
 
 
 
1323
 
1324
 
1325
+ @app.get("/rate-limits", include_in_schema=False)
1326
+ async def rate_limits_legacy():
1327
+ return await api_rate_limits()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1328
 
1329
 
1330
+ @app.get("/logs", include_in_schema=False)
1331
+ async def logs_legacy(type: str = "all"):
1332
+ return await api_logs(type=type)
1333
+
1334
+
1335
+ @app.get("/alerts", include_in_schema=False)
1336
+ async def alerts_legacy():
1337
+ return await api_alerts()
1338
+
1339
+
1340
+ @app.get("/hf/registry", include_in_schema=False)
1341
+ async def hf_registry_legacy(type: str = "models"):
1342
+ return await hf_registry(type=type)
1343
+
1344
+
1345
+ @app.post("/hf/refresh", include_in_schema=False)
1346
+ async def hf_refresh_legacy():
1347
+ return await hf_refresh()
1348
+
1349
+
1350
+ @app.get("/hf/search", include_in_schema=False)
1351
+ async def hf_search_legacy(q: str = "", kind: str = "models"):
1352
+ return await hf_search(q=q, kind=kind)
1353
+
1354
+
1355
+ # Serve static files (JS, CSS, etc.)
1356
+ # Serve static files (JS, CSS, etc.)
1357
+ if os.path.exists("static"):
1358
+ app.mount("/static", StaticFiles(directory="static"), name="static")
1359
+
1360
+ # Serve config.js
1361
+ @app.get("/config.js")
1362
+ async def config_js():
1363
+ try:
1364
+ with open("config.js", "r", encoding="utf-8") as f:
1365
+ return Response(content=f.read(), media_type="application/javascript")
1366
+ except:
1367
+ return Response(content="// Config not found", media_type="application/javascript")
1368
+
1369
+ # API v2 endpoints for enhanced dashboard
1370
+ @app.get("/api/v2/status")
1371
+ async def v2_status():
1372
+ """Enhanced status endpoint"""
1373
+ providers = await get_provider_stats()
1374
  return {
1375
+ "services": {
1376
+ "config_loader": {
1377
+ "apis_loaded": len(providers),
1378
+ "status": "active"
1379
+ },
1380
+ "scheduler": {
1381
+ "total_tasks": len(providers),
1382
+ "status": "active"
1383
+ },
1384
+ "persistence": {
1385
+ "cached_apis": len(providers),
1386
+ "status": "active"
1387
+ },
1388
+ "websocket": {
1389
+ "total_connections": len(manager.active_connections),
1390
+ "status": "active"
1391
+ }
1392
+ },
1393
+ "timestamp": datetime.now().isoformat()
1394
  }
1395
 
1396
+ @app.get("/api/v2/config/apis")
1397
+ async def v2_config_apis():
1398
+ """Get API configuration"""
1399
+ providers = await get_provider_stats()
1400
+ apis = {}
1401
+ for p in providers:
1402
+ apis[p["name"].lower().replace(" ", "_")] = {
1403
+ "name": p["name"],
1404
+ "category": p["category"],
1405
+ "base_url": p.get("base_url", ""),
1406
+ "status": p["status"]
1407
+ }
1408
+ return {"apis": apis}
1409
 
1410
+ @app.get("/api/v2/schedule/tasks")
1411
+ async def v2_schedule_tasks():
1412
+ """Get scheduled tasks"""
1413
+ providers = await get_provider_stats()
1414
+ tasks = {}
1415
+ for p in providers:
1416
+ api_id = p["name"].lower().replace(" ", "_")
1417
+ tasks[api_id] = {
1418
+ "api_id": api_id,
1419
+ "interval": 300,
1420
+ "enabled": True,
1421
+ "last_status": "success",
1422
+ "last_run": datetime.now().isoformat()
1423
+ }
1424
+ return tasks
1425
 
1426
+ @app.get("/api/v2/schedule/tasks/{api_id}")
1427
+ async def v2_schedule_task(api_id: str):
1428
+ """Get specific scheduled task"""
1429
+ return {
1430
+ "api_id": api_id,
1431
+ "interval": 300,
1432
+ "enabled": True,
1433
+ "last_status": "success",
1434
+ "last_run": datetime.now().isoformat()
1435
+ }
1436
 
1437
+ @app.put("/api/v2/schedule/tasks/{api_id}")
1438
+ async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = True):
1439
+ """Update schedule"""
1440
+ return {
1441
+ "api_id": api_id,
1442
+ "interval": interval,
1443
+ "enabled": enabled,
1444
+ "message": "Schedule updated"
1445
+ }
1446
 
1447
+ @app.post("/api/v2/schedule/tasks/{api_id}/force-update")
1448
+ async def v2_force_update(api_id: str):
1449
+ """Force update for specific API"""
1450
+ return {
1451
+ "api_id": api_id,
1452
+ "status": "updated",
1453
+ "timestamp": datetime.now().isoformat()
1454
+ }
1455
 
1456
+ @app.post("/api/v2/export/json")
1457
+ async def v2_export_json(request: dict):
1458
+ """Export data as JSON"""
1459
+ market = await get_market_data()
1460
  return {
1461
+ "filepath": "export.json",
1462
+ "download_url": "/api/v2/export/download/export.json",
1463
+ "timestamp": datetime.now().isoformat()
 
 
 
1464
  }
1465
 
1466
+ @app.post("/api/v2/export/csv")
1467
+ async def v2_export_csv(request: dict):
1468
+ """Export data as CSV"""
1469
+ return {
1470
+ "filepath": "export.csv",
1471
+ "download_url": "/api/v2/export/download/export.csv",
1472
+ "timestamp": datetime.now().isoformat()
1473
+ }
1474
 
1475
+ @app.post("/api/v2/backup")
1476
+ async def v2_backup():
1477
+ """Create backup"""
1478
+ return {
1479
+ "backup_file": f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
1480
+ "timestamp": datetime.now().isoformat()
1481
+ }
1482
 
1483
+ @app.post("/api/v2/cleanup/cache")
1484
+ async def v2_cleanup_cache():
1485
+ """Clear cache"""
1486
+ # Clear all caches
1487
+ for key in cache:
1488
+ cache[key]["data"] = None
1489
+ cache[key]["timestamp"] = None
1490
+ return {
1491
+ "status": "cleared",
1492
+ "timestamp": datetime.now().isoformat()
1493
+ }
1494
 
1495
+ @app.websocket("/api/v2/ws")
1496
+ async def v2_websocket(websocket: WebSocket):
1497
+ """Enhanced WebSocket endpoint"""
1498
+ await manager.connect(websocket)
1499
+ try:
1500
+ while True:
1501
+ await asyncio.sleep(5)
1502
+
1503
+ # Send status update
1504
+ await websocket.send_json({
1505
+ "type": "status_update",
1506
+ "data": {
1507
+ "timestamp": datetime.now().isoformat()
1508
+ }
1509
+ })
1510
+
1511
+ except WebSocketDisconnect:
1512
+ manager.disconnect(websocket)
1513
+
1514
+ # Pool Management Helpers and Endpoints
1515
+ def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
1516
+ members_payload = []
1517
+ current_provider = None
1518
+
1519
+ for member in pool.get("members", []):
1520
+ provider_id = member["provider_id"]
1521
+ provider_status = provider_map.get(provider_id)
1522
+
1523
+ status = provider_status["status"] if provider_status else "unknown"
1524
+ uptime = provider_status.get("uptime", member.get("success_rate", 0)) if provider_status else member.get("success_rate", 0)
1525
+ response_time = provider_status.get("response_time_ms") if provider_status else None
1526
+
1527
+ member_payload = {
1528
+ "provider_id": provider_id,
1529
+ "provider_name": member["provider_name"],
1530
+ "priority": member.get("priority", 1),
1531
+ "weight": member.get("weight", 1),
1532
+ "use_count": member.get("use_count", 0),
1533
+ "success_rate": round(uptime, 2) if uptime is not None else 0,
1534
+ "status": status,
1535
+ "response_time_ms": response_time,
1536
+ "rate_limit": {
1537
+ "usage": member.get("rate_limit_usage", 0),
1538
+ "limit": member.get("rate_limit_limit", 0),
1539
+ "percentage": member.get("rate_limit_percentage", 0)
1540
+ }
1541
+ }
1542
 
1543
+ # keep database stats in sync
1544
+ db.update_member_stats(
1545
+ pool["id"],
1546
+ provider_id,
1547
+ success_rate=uptime,
1548
+ rate_limit_usage=member_payload["rate_limit"]["usage"],
1549
+ rate_limit_limit=member_payload["rate_limit"]["limit"],
1550
+ rate_limit_percentage=member_payload["rate_limit"]["percentage"],
1551
+ )
1552
 
1553
+ members_payload.append(member_payload)
 
 
1554
 
1555
+ if not current_provider and status == "online":
1556
+ current_provider = {"name": member["provider_name"], "status": status}
 
 
 
 
1557
 
1558
+ if not current_provider and members_payload:
1559
+ degraded_member = next((m for m in members_payload if m["status"] == "degraded"), None)
1560
+ if degraded_member:
1561
+ current_provider = {"name": degraded_member["provider_name"], "status": degraded_member["status"]}
1562
 
1563
+ return {
1564
+ "pool_id": pool["id"],
1565
+ "pool_name": pool["name"],
1566
+ "category": pool["category"],
1567
+ "rotation_strategy": pool["rotation_strategy"],
1568
+ "description": pool.get("description"),
1569
+ "enabled": bool(pool.get("enabled", 1)),
1570
+ "members": members_payload,
1571
+ "current_provider": current_provider,
1572
+ "total_rotations": pool.get("rotation_count", 0),
1573
+ "created_at": pool.get("created_at")
1574
+ }
1575
 
1576
 
1577
+ def transform_rotation_history(entries: List[Dict]) -> List[Dict]:
1578
+ history = []
1579
+ for entry in entries:
1580
+ history.append({
1581
+ "pool_id": entry["pool_id"],
1582
+ "provider_id": entry["provider_id"],
1583
+ "provider_name": entry["provider_name"],
1584
+ "reason": entry["reason"],
1585
+ "timestamp": entry["created_at"]
1586
  })
1587
+ return history
1588
+
1589
+
1590
+ async def broadcast_pool_update(action: str, pool_id: int, extra: Optional[Dict] = None):
1591
+ payload = {"type": "pool_update", "action": action, "pool_id": pool_id}
1592
+ if extra:
1593
+ payload.update(extra)
1594
+ await manager.broadcast(payload)
1595
+
1596
+
1597
+ @app.get("/api/pools")
1598
+ async def get_pools():
1599
+ """Get all pools"""
1600
+ providers = await get_provider_stats()
1601
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1602
+ pools = db.get_pools()
1603
+ response = [build_pool_payload(pool, provider_map) for pool in pools]
1604
+ return {"pools": response}
1605
+
1606
+
1607
+ @app.post("/api/pools")
1608
+ async def create_pool(pool: PoolCreate):
1609
+ """Create a new pool"""
1610
+ valid_strategies = {"round_robin", "priority", "weighted", "least_used"}
1611
+ if pool.rotation_strategy not in valid_strategies:
1612
+ raise HTTPException(status_code=400, detail="Invalid rotation strategy")
1613
+
1614
+ pool_id = db.create_pool(
1615
+ name=pool.name,
1616
+ category=pool.category,
1617
+ rotation_strategy=pool.rotation_strategy,
1618
+ description=pool.description,
1619
+ enabled=True
1620
+ )
1621
 
1622
+ providers = await get_provider_stats()
1623
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1624
+ pool_record = db.get_pool(pool_id)
1625
+ payload = build_pool_payload(pool_record, provider_map)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1626
 
1627
+ await broadcast_pool_update("created", pool_id, {"pool": payload})
1628
+
1629
+ return {
1630
+ "pool_id": pool_id,
1631
+ "message": "Pool created successfully",
1632
+ "pool": payload
1633
+ }
1634
+
1635
+
1636
+ @app.get("/api/pools/{pool_id}")
1637
+ async def get_pool(pool_id: int):
1638
+ """Get specific pool"""
1639
+ pool = db.get_pool(pool_id)
1640
+ if not pool:
1641
+ raise HTTPException(status_code=404, detail="Pool not found")
1642
+
1643
+ providers = await get_provider_stats()
1644
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1645
+ return build_pool_payload(pool, provider_map)
1646
+
1647
+
1648
+ @app.delete("/api/pools/{pool_id}")
1649
+ async def delete_pool(pool_id: int):
1650
+ """Delete a pool"""
1651
+ pool = db.get_pool(pool_id)
1652
+ if not pool:
1653
+ raise HTTPException(status_code=404, detail="Pool not found")
1654
+
1655
+ db.delete_pool(pool_id)
1656
+ await broadcast_pool_update("deleted", pool_id)
1657
+ return {"message": "Pool deleted successfully"}
1658
+
1659
+
1660
+ @app.post("/api/pools/{pool_id}/members")
1661
+ async def add_pool_member(pool_id: int, member: PoolMemberAdd):
1662
+ """Add a member to a pool"""
1663
+ pool = db.get_pool(pool_id)
1664
+ if not pool:
1665
+ raise HTTPException(status_code=404, detail="Pool not found")
1666
+
1667
+ providers = await get_provider_stats()
1668
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1669
+ provider_info = provider_map.get(member.provider_id)
1670
+ if not provider_info:
1671
+ raise HTTPException(status_code=404, detail="Provider not found")
1672
+
1673
+ existing = next((m for m in pool["members"] if m["provider_id"] == member.provider_id), None)
1674
+ if existing:
1675
+ raise HTTPException(status_code=400, detail="Provider already in pool")
1676
+
1677
+ db.add_pool_member(
1678
+ pool_id=pool_id,
1679
+ provider_id=member.provider_id,
1680
+ provider_name=provider_info["name"],
1681
+ priority=max(1, min(member.priority, 10)),
1682
+ weight=max(1, min(member.weight, 100)),
1683
+ success_rate=provider_info.get("uptime", 0),
1684
+ rate_limit_usage=provider_info.get("rate_limit", {}).get("usage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
1685
+ rate_limit_limit=provider_info.get("rate_limit", {}).get("limit", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
1686
+ rate_limit_percentage=provider_info.get("rate_limit", {}).get("percentage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
1687
+ )
1688
+
1689
+ pool_record = db.get_pool(pool_id)
1690
+ payload = build_pool_payload(pool_record, provider_map)
1691
+ await broadcast_pool_update("member_added", pool_id, {"provider_id": member.provider_id})
1692
+
1693
+ return {
1694
+ "message": "Member added successfully",
1695
+ "pool": payload
1696
+ }
1697
+
1698
+
1699
+ @app.delete("/api/pools/{pool_id}/members/{provider_id}")
1700
+ async def remove_pool_member(pool_id: int, provider_id: str):
1701
+ """Remove a member from a pool"""
1702
+ pool = db.get_pool(pool_id)
1703
+ if not pool:
1704
+ raise HTTPException(status_code=404, detail="Pool not found")
1705
+
1706
+ db.remove_pool_member(pool_id, provider_id)
1707
+ await broadcast_pool_update("member_removed", pool_id, {"provider_id": provider_id})
1708
+
1709
+ providers = await get_provider_stats()
1710
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1711
+ pool_record = db.get_pool(pool_id)
1712
+ payload = build_pool_payload(pool_record, provider_map)
1713
+
1714
+ return {
1715
+ "message": "Member removed successfully",
1716
+ "pool": payload
1717
+ }
1718
+
1719
+
1720
+ @app.post("/api/pools/{pool_id}/rotate")
1721
+ async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
1722
+ """Rotate pool to next provider"""
1723
+ pool = db.get_pool(pool_id)
1724
+ if not pool:
1725
+ raise HTTPException(status_code=404, detail="Pool not found")
1726
+
1727
+ if not pool["members"]:
1728
+ raise HTTPException(status_code=400, detail="Pool has no members")
1729
+
1730
+ providers = await get_provider_stats(force_refresh=True)
1731
+ provider_map = {provider_slug(p["name"]): p for p in providers}
1732
+
1733
+ members_with_status = []
1734
+ for member in pool["members"]:
1735
+ status_info = provider_map.get(member["provider_id"])
1736
+ if status_info:
1737
+ members_with_status.append((member, status_info))
1738
+
1739
+ online_members = [m for m in members_with_status if m[1]["status"] == "online"]
1740
+ degraded_members = [m for m in members_with_status if m[1]["status"] == "degraded"]
1741
+
1742
+ candidates = online_members or degraded_members
1743
+ if not candidates:
1744
+ raise HTTPException(status_code=400, detail="No healthy providers available for rotation")
1745
+
1746
+ strategy = pool.get("rotation_strategy", "round_robin")
1747
+
1748
+ if strategy == "priority":
1749
+ candidates.sort(key=lambda x: (x[0].get("priority", 1), x[0].get("weight", 1)), reverse=True)
1750
+ selected_member, status_info = candidates[0]
1751
+ elif strategy == "weighted":
1752
+ weights = [max(1, c[0].get("weight", 1)) for c in candidates]
1753
+ total_weight = sum(weights)
1754
+ roll = random.uniform(0, total_weight)
1755
+ cumulative = 0
1756
+ selected_member = candidates[0][0]
1757
+ status_info = candidates[0][1]
1758
+ for (candidate, status), weight in zip(candidates, weights):
1759
+ cumulative += weight
1760
+ if roll <= cumulative:
1761
+ selected_member, status_info = candidate, status
1762
+ break
1763
+ elif strategy == "least_used":
1764
+ candidates.sort(key=lambda x: x[0].get("use_count", 0))
1765
+ selected_member, status_info = candidates[0]
1766
+ else: # round_robin or default
1767
+ candidates.sort(key=lambda x: x[0].get("use_count", 0))
1768
+ selected_member, status_info = candidates[0]
1769
+
1770
+ db.increment_member_use(pool_id, selected_member["provider_id"])
1771
+ db.update_member_stats(
1772
+ pool_id,
1773
+ selected_member["provider_id"],
1774
+ success_rate=status_info.get("uptime", selected_member.get("success_rate")),
1775
+ rate_limit_usage=status_info.get("rate_limit", {}).get("usage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
1776
+ rate_limit_limit=status_info.get("rate_limit", {}).get("limit", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
1777
+ rate_limit_percentage=status_info.get("rate_limit", {}).get("percentage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
1778
+ )
1779
+ db.log_pool_rotation(
1780
+ pool_id,
1781
+ selected_member["provider_id"],
1782
+ selected_member["provider_name"],
1783
+ request.get("reason", "manual") if request else "manual"
1784
  )
1785
 
1786
+ pool_record = db.get_pool(pool_id)
1787
+ payload = build_pool_payload(pool_record, provider_map)
1788
+
1789
+ await broadcast_pool_update("rotated", pool_id, {
1790
+ "provider_id": selected_member["provider_id"],
1791
+ "provider_name": selected_member["provider_name"]
1792
+ })
1793
+
1794
+ return {
1795
+ "message": "Pool rotated successfully",
1796
+ "provider_name": selected_member["provider_name"],
1797
+ "provider_id": selected_member["provider_id"],
1798
+ "total_rotations": pool_record.get("rotation_count", 0),
1799
+ "pool": payload
1800
+ }
1801
 
 
 
 
 
 
 
 
1802
 
1803
+ @app.get("/api/pools/{pool_id}/history")
1804
+ async def get_pool_history(pool_id: int, limit: int = 20):
1805
+ """Get rotation history for a pool"""
1806
+ try:
1807
+ raw_history = db.get_pool_rotation_history(pool_id, limit)
1808
+ except Exception as exc: # pragma: no cover - defensive
1809
+ logger.warning("pool history fetch failed for %s: %s", pool_id, exc)
1810
+ raw_history = []
1811
+ history = transform_rotation_history(raw_history)
1812
+ return {
1813
+ "history": history,
1814
+ "total": len(history)
1815
+ }
1816
+
1817
+
1818
+ @app.get("/api/pools/history")
1819
+ async def get_all_history(limit: int = 50):
1820
+ """Get all rotation history"""
1821
+ try:
1822
+ raw_history = db.get_pool_rotation_history(None, limit)
1823
+ except Exception as exc: # pragma: no cover - defensive
1824
+ logger.warning("global pool history fetch failed: %s", exc)
1825
+ raw_history = []
1826
+ history = transform_rotation_history(raw_history)
1827
+ return {
1828
+ "history": history,
1829
+ "total": len(history)
1830
+ }
1831
 
1832
  if __name__ == "__main__":
1833
+ print("🚀 Crypto Monitor ULTIMATE")
1834
+ print("📊 Real APIs: CoinGecko, CoinCap, Binance, DeFi Llama, Fear & Greed")
1835
+ print("🌐 http://localhost:8000/dashboard")
1836
+ print("📡 API Docs: http://localhost:8000/docs")
1837
+ uvicorn.run(app, host="0.0.0.0", port=8000)
1838
+
1839
+ # === Compatibility routes without /api prefix for frontend fallbacks ===
1840
+
1841
+ @app.get("/providers")
1842
+ async def providers_root():
1843
+ """Compatibility: mirror /api/providers at /providers"""
1844
+ return await providers()
1845
+
1846
+ @app.get("/providers/health")
1847
+ async def providers_health_root():
1848
+ """Compatibility: health-style endpoint for providers"""
1849
+ data = await get_provider_stats(force_refresh=True)
1850
+ return data
1851
+
1852
+ @app.get("/categories")
1853
+ async def categories_root():
1854
+ """Compatibility: mirror /api/categories at /categories"""
1855
+ return await api_categories()
1856
+
1857
+ @app.get("/rate-limits")
1858
+ async def rate_limits_root():
1859
+ """Compatibility: mirror /api/rate-limits at /rate-limits"""
1860
+ return await api_rate_limits()
1861
+
1862
+ @app.get("/logs")
1863
+ async def logs_root(type: str = "all"):
1864
+ """Compatibility: mirror /api/logs at /logs"""
1865
+ return await api_logs(type=type)
1866
+
1867
+ @app.get("/alerts")
1868
+ async def alerts_root():
1869
+ """Compatibility: mirror /api/alerts at /alerts"""
1870
+ return await api_alerts()
1871
+
1872
+ @app.get("/hf/health")
1873
+ async def hf_health_root():
1874
+ """Compatibility: mirror /api/hf/health at /hf/health"""
1875
+ return await hf_health()
1876
+
1877
+ @app.get("/hf/registry")
1878
+ async def hf_registry_root(type: str = "models"):
1879
+ """Compatibility: mirror /api/hf/registry at /hf/registry"""
1880
+ return await hf_registry(type=type)
1881
+
1882
+ @app.get("/hf/search")
1883
+ async def hf_search_root(q: str = "", kind: str = "models"):
1884
+ """Compatibility: mirror /api/hf/search at /hf/search"""
1885
+ return await hf_search(q=q, kind=kind)
1886
+
1887
+ @app.post("/hf/run-sentiment")
1888
+ async def hf_run_sentiment_root(request: SentimentRequest):
1889
+ """Compatibility: mirror /api/hf/run-sentiment at /hf/run-sentiment"""
1890
+ return await hf_run_sentiment(request)
templates/index.html CHANGED
The diff for this file is too large to render. See raw diff
 
templates/main.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from importlib import util
2
+ from pathlib import Path
3
+ import sys
4
+
5
+
6
+ def _load_app_module():
7
+ """
8
+ تلاش برای وارد کردن آبجکت FastAPI با نام app.
9
+ ابتدا سعی می‌کنیم مثل قبل از ماژول «app» ایمپورت کنیم.
10
+ اگر نام «app» به پوشه‌ای اشاره کند و attribute نداشته باشد،
11
+ فایل app.py را به طور مستقیم بارگذاری می‌کنیم.
12
+ """
13
+ try:
14
+ from app import app as fastapi_app # type: ignore
15
+ return fastapi_app
16
+ except (ImportError, AttributeError):
17
+ current_dir = Path(__file__).resolve().parent
18
+ app_path = current_dir / "app.py"
19
+ spec = util.spec_from_file_location("crypto_monitor_app", app_path)
20
+ if spec is None or spec.loader is None:
21
+ raise ImportError("Could not load app.py module for FastAPI application.")
22
+ module = util.module_from_spec(spec)
23
+ sys.modules["crypto_monitor_app"] = module
24
+ spec.loader.exec_module(module)
25
+ if not hasattr(module, "app"):
26
+ raise ImportError("app.py does not define an 'app' FastAPI instance.")
27
+ return module.app # type: ignore[attr-defined]
28
+
29
+
30
+ app = _load_app_module()