Really-amin commited on
Commit
45ad67c
·
verified ·
1 Parent(s): 57a94bf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +345 -133
app.py CHANGED
@@ -1,19 +1,19 @@
1
- from fastapi import FastAPI, WebSocket, WebSocketDisconnect
2
  from fastapi.staticfiles import StaticFiles
3
  from fastapi.responses import FileResponse, JSONResponse
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
6
  from datetime import datetime, timedelta, timezone
7
- from typing import Any, Dict, List
8
 
9
  import asyncio
10
- import ccxt
11
  import random
 
12
 
13
  app = FastAPI(title="Crypto Data Source API")
14
 
15
  # ------------------------------
16
- # CORS middleware
17
  # ------------------------------
18
  app.add_middleware(
19
  CORSMiddleware,
@@ -29,38 +29,101 @@ app.add_middleware(
29
  BASE_DIR = Path(__file__).resolve().parent
30
  INDEX_FILE = BASE_DIR / "index.html"
31
  STATIC_DIR = BASE_DIR / "static"
32
-
33
  START_TIME = datetime.now(timezone.utc)
34
 
35
- # اگر static directory وجود دارد، برای فایل‌های استاتیک mount می‌کنیم
36
  if STATIC_DIR.exists() and STATIC_DIR.is_dir():
37
  app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  # ------------------------------
41
- # API های پایه (health / markets / ticker)
42
  # ------------------------------
43
  @app.get("/api/health")
44
  async def health_check():
45
- """ساده‌ترین health endpoint برای چک وضعیت و نسخه ccxt"""
46
  return {
47
  "status": "online",
48
  "version": "1.0.0",
49
- "timestamp": datetime.now(timezone.utc).isoformat(),
50
  "ccxt_version": ccxt.__version__,
51
  }
52
 
53
 
54
  @app.get("/api/markets")
55
  async def get_markets():
56
- """نمونه‌ی گرفتن مارکت‌ها از بایننس (با ccxt)"""
57
  try:
58
  exchange = ccxt.binance()
59
  markets = exchange.load_markets()
60
  return {
61
  "success": True,
62
  "total_markets": len(markets),
63
- "markets": list(markets.keys())[:50], # فقط ۵۰ تای اول
64
  }
65
  except Exception as e: # noqa: BLE001
66
  return {"success": False, "error": str(e)}
@@ -68,7 +131,7 @@ async def get_markets():
68
 
69
  @app.get("/api/ticker/{symbol}")
70
  async def get_ticker(symbol: str):
71
- """نمونه‌ی گرفتن تیکر برای یک symbol مشخص (مثلاً BTC/USDT)"""
72
  try:
73
  exchange = ccxt.binance()
74
  ticker = exchange.fetch_ticker(symbol)
@@ -78,141 +141,302 @@ async def get_ticker(symbol: str):
78
 
79
 
80
  # ------------------------------
81
- # API هایی که فرانت انتظار دارد
82
- # /api/status
83
- # /api/categories
84
- # /api/charts/health-history
85
- # /api/charts/compliance
86
  # ------------------------------
87
-
88
  @app.get("/api/status")
89
  async def api_status():
90
- """
91
- وضعیت کلی سیستم برای داشبورد.
92
- فرانت معمولاً از این برای کارت‌های Status / Uptime / Errors استفاده می‌کند.
93
- """
94
- now = datetime.now(timezone.utc)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  uptime_seconds = (now - START_TIME).total_seconds()
96
- uptime_human = str(timedelta(seconds=int(uptime_seconds)))
97
 
98
- # این مقادیر می‌تونه بعداً به داده‌های واقعی متصل شود
99
  return {
100
  "status": "online",
101
  "service": "crypto-data-source",
102
  "version": "1.0.0",
103
  "timestamp": now.isoformat(),
104
  "uptime_seconds": uptime_seconds,
105
- "uptime_human": uptime_human,
106
- "components": {
107
- "api": "healthy",
108
- "market_data": "healthy",
109
- "websocket": "healthy",
110
- "database": "ok",
111
- },
112
- "metrics": {
113
- "requests_per_minute": random.randint(5, 25),
114
- "error_rate_percent": round(random.uniform(0.0, 2.5), 2),
115
- "avg_latency_ms": random.randint(100, 400),
116
- },
117
  }
118
 
119
 
120
- @app.get("/api/categories")
121
- async def api_categories():
122
- """
123
- لیست دسته‌بندی‌هایی که داشبورد می‌خواهد نشان دهد
124
- (برای فیلترها / تب‌ها / کارت‌ها).
125
  """
126
- categories: List[Dict[str, Any]] = [
127
- {
128
- "id": "health",
129
- "name": "System Health",
130
- "description": "Overall status of all data providers and services.",
131
- "enabled": True,
132
- },
133
- {
134
- "id": "market_data",
135
- "name": "Market Data",
136
- "description": "Spot & derivatives price feeds from crypto exchanges.",
137
- "enabled": True,
138
- },
139
- {
140
- "id": "compliance",
141
- "name": "Compliance & Limits",
142
- "description": "Rate limits, API key usage and provider compliance checks.",
143
- "enabled": True,
144
- },
145
  ]
146
- return {"success": True, "categories": categories}
 
147
 
148
 
149
- def _generate_time_series_points(
150
- hours: int,
151
- base_value: float,
152
- noise: float,
153
- kind: str = "score",
154
- ) -> List[Dict[str, Any]]:
155
  """
156
- ساخت یک سری زمانی ساده برای نمودارها.
 
 
157
  """
158
- now = datetime.now(timezone.utc)
159
- points: List[Dict[str, Any]] = []
160
- total_points = max(12, hours) # حداقل ۱۲ نقطه
161
-
162
- for i in range(total_points):
163
- ts = now - timedelta(hours=hours * (total_points - 1 - i) / total_points)
164
- value = base_value + random.uniform(-noise, noise)
165
- value = max(0.0, min(1.0, value)) # clamp بین ۰ و ۱
166
-
167
- if kind == "score":
168
- point = {
169
- "timestamp": ts.isoformat(),
170
- "score": round(value, 3),
171
- "status": "ok" if value > 0.6 else "degraded",
172
- }
173
- else: # compliance
174
- point = {
175
- "timestamp": ts.isoformat(),
176
- "compliance_ratio": round(value, 3),
177
- "violations": random.randint(0, 3) if value < 0.8 else 0,
 
 
178
  }
179
- points.append(point)
180
 
181
- return points
182
 
183
 
 
 
 
184
  @app.get("/api/charts/health-history")
185
  async def api_chart_health_history(hours: int = 24):
186
  """
187
- داده‌های نمودار سری زمانی برای سلامت سیستم در X ساعت گذشته.
 
188
  """
189
- points = _generate_time_series_points(hours=hours, base_value=0.9, noise=0.1, kind="score")
190
- return {
191
- "success": True,
192
- "range_hours": hours,
193
- "series": points,
194
- }
 
 
 
 
 
 
195
 
 
196
 
 
 
 
 
197
  @app.get("/api/charts/compliance")
198
  async def api_chart_compliance(days: int = 7):
199
  """
200
- داده‌های نمودار سری زمانی برای وضعیت کامپلاینس در X روز گذشته.
 
201
  """
202
- # تبدیل days به hours برای ساخت سری
203
- hours = days * 24
204
- points = _generate_time_series_points(hours=hours, base_value=0.85, noise=0.15, kind="compliance")
205
- return {
206
- "success": True,
207
- "range_days": days,
208
- "series": points,
209
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
 
211
 
212
  # ------------------------------
213
- # WebSocket /ws/liveبرای داشبورد زنده
214
  # ------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  class LiveConnections:
217
  def __init__(self) -> None:
218
  self.active: List[WebSocket] = []
@@ -230,7 +454,6 @@ class LiveConnections:
230
  try:
231
  await ws.send_json(message)
232
  except Exception:
233
- # اگر ارسال به هر دلیلی fail شد، آن اتصال را حذف می‌کنیم
234
  self.disconnect(ws)
235
 
236
 
@@ -240,29 +463,26 @@ live_manager = LiveConnections()
240
  @app.websocket("/ws/live")
241
  async def websocket_live(websocket: WebSocket):
242
  """
243
- WebSocket ساده برای فرانت که هر چند ثانیه یک بار
244
- وضعیت/متریک‌های زنده را ارسال می‌کند.
245
  """
246
  await live_manager.connect(websocket)
247
  try:
248
- # پیام خوشامد
249
  await websocket.send_json(
250
  {
251
  "type": "welcome",
252
  "service": "crypto-data-source",
253
- "timestamp": datetime.now(timezone.utc).isoformat(),
254
  }
255
  )
256
 
257
  while True:
258
- # اینجا می‌تونی بعداً داده واقعی مثل health providers, latency و ... بفرستی
259
  payload = {
260
  "type": "live_metrics",
261
- "timestamp": datetime.now(timezone.utc).isoformat(),
262
  "metrics": {
263
  "cpu_usage": random.randint(5, 40),
264
- "memory_usage": random.randint(20, 70),
265
- "active_requests": random.randint(1, 20),
266
  "error_rate_percent": round(random.uniform(0.0, 3.0), 2),
267
  },
268
  }
@@ -275,21 +495,17 @@ async def websocket_live(websocket: WebSocket):
275
 
276
 
277
  # ------------------------------
278
- # Frontend routing (بدون پوشه frontend/)
279
  # ------------------------------
280
-
281
  @app.get("/")
282
  async def root():
283
- """
284
- اگر index.html کنار همین فایل باشد، همان را برمی‌گرداند.
285
- در غیر این صورت، یک پیام JSON برمی‌گرداند.
286
- """
287
  if INDEX_FILE.exists():
288
  return FileResponse(INDEX_FILE)
289
  return JSONResponse(
290
  {
291
  "message": "No frontend index.html found in project root.",
292
- "hint": "Place an index.html file next to this Python file or use /docs for the API.",
293
  "api_docs": "/docs",
294
  }
295
  )
@@ -298,29 +514,25 @@ async def root():
298
  @app.get("/{full_path:path}")
299
  async def spa_catch_all(full_path: str):
300
  """
301
- برای روتینگ سمت فرانت (SPA):
302
- - اگر مسیر با api/ شروع شود و endpointی وجود نداشته باشد، 404 منطقی می‌دهیم
303
- - اگر فایلی با همان مسیر داخل static وجود داشته باشد، همان فایل سرو می‌شود
304
- - در غیر این صورت، اگر index.html وجود داشته باشد، همان برگردانده می‌شود
305
  """
306
- # اگر مسیر API است، ولی endpoint تعریف نشده:
307
  if full_path.startswith("api/"):
308
  return JSONResponse(
309
  {"error": "API endpoint not found", "path": full_path},
310
  status_code=404,
311
  )
312
 
313
- # اگر فایل static موجود است
314
  if STATIC_DIR.exists():
315
  candidate_file = STATIC_DIR / full_path
316
  if candidate_file.is_file():
317
  return FileResponse(candidate_file)
318
 
319
- # در نهایت، اگر index.html داریم، به عنوان SPA برمی‌گردانیم
320
  if INDEX_FILE.exists():
321
  return FileResponse(INDEX_FILE)
322
 
323
- # هیچ چیز پیدا نشد:
324
  return JSONResponse(
325
  {"error": "No frontend available for this path.", "path": full_path},
326
  status_code=404,
@@ -328,7 +540,7 @@ async def spa_catch_all(full_path: str):
328
 
329
 
330
  # ------------------------------
331
- # Local run
332
  # ------------------------------
333
  if __name__ == "__main__":
334
  import uvicorn
 
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
2
  from fastapi.staticfiles import StaticFiles
3
  from fastapi.responses import FileResponse, JSONResponse
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pathlib import Path
6
  from datetime import datetime, timedelta, timezone
7
+ from typing import Any, Dict, List, Optional
8
 
9
  import asyncio
 
10
  import random
11
+ import ccxt
12
 
13
  app = FastAPI(title="Crypto Data Source API")
14
 
15
  # ------------------------------
16
+ # CORS
17
  # ------------------------------
18
  app.add_middleware(
19
  CORSMiddleware,
 
29
  BASE_DIR = Path(__file__).resolve().parent
30
  INDEX_FILE = BASE_DIR / "index.html"
31
  STATIC_DIR = BASE_DIR / "static"
 
32
  START_TIME = datetime.now(timezone.utc)
33
 
 
34
  if STATIC_DIR.exists() and STATIC_DIR.is_dir():
35
  app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
36
 
37
+ # ------------------------------
38
+ # داده‌های نمونه برای Providerها / Keys
39
+ # ------------------------------
40
+ BASE_PROVIDERS: List[Dict[str, Any]] = [
41
+ {"name": "Binance Spot", "category": "market_data", "has_key": False, "priority": 5},
42
+ {"name": "Binance Futures", "category": "market_data", "has_key": False, "priority": 4},
43
+ {"name": "CoinGecko", "category": "market_data", "has_key": False, "priority": 4},
44
+ {"name": "CoinPaprika", "category": "market_data", "has_key": False, "priority": 3},
45
+ {"name": "Etherscan", "category": "blockchain", "has_key": True, "priority": 4},
46
+ {"name": "BscScan", "category": "blockchain", "has_key": True, "priority": 3},
47
+ {"name": "TronScan", "category": "blockchain", "has_key": False, "priority": 3},
48
+ {"name": "CryptoPanic", "category": "news", "has_key": False, "priority": 2},
49
+ ]
50
+
51
+ CUSTOM_APIS: List[Dict[str, Any]] = []
52
+
53
+ API_KEYS: List[Dict[str, str]] = [
54
+ {"provider": "Binance", "key_masked": "BINANCE-****-****-1234"},
55
+ {"provider": "Etherscan", "key_masked": "ETHERSCAN-****-****-ABCD"},
56
+ {"provider": "BscScan", "key_masked": "BSCSCAN-****-****-5678"},
57
+ ]
58
+
59
+
60
+ def _utc_now() -> datetime:
61
+ return datetime.now(timezone.utc)
62
+
63
+
64
+ # ------------------------------
65
+ # کمکی: ساخت snapshot از provider ها
66
+ # ------------------------------
67
+ def _generate_providers_snapshot() -> List[Dict[str, Any]]:
68
+ now = _utc_now()
69
+ providers: List[Dict[str, Any]] = []
70
+
71
+ all_providers = BASE_PROVIDERS + CUSTOM_APIS
72
+
73
+ for base in all_providers:
74
+ # response time between 80ms and 900ms
75
+ rt = random.randint(80, 900)
76
+ # staleness between 0 and 30 minutes
77
+ minutes_ago = random.uniform(0, 30)
78
+ last_fetch_dt = now - timedelta(minutes=minutes_ago)
79
+
80
+ # تعیین وضعیت بر اساس response time
81
+ if rt < 300:
82
+ status = "online"
83
+ elif rt < 700:
84
+ status = "degraded"
85
+ else:
86
+ status = "offline"
87
+
88
+ providers.append(
89
+ {
90
+ "name": base["name"],
91
+ "category": base["category"],
92
+ "status": status,
93
+ "response_time_ms": rt,
94
+ "last_fetch": last_fetch_dt.isoformat(),
95
+ "has_key": bool(base.get("has_key", False)),
96
+ "priority": int(base.get("priority", 3)),
97
+ }
98
+ )
99
+
100
+ return providers
101
+
102
 
103
  # ------------------------------
104
+ # APIهای پایه ccxt
105
  # ------------------------------
106
  @app.get("/api/health")
107
  async def health_check():
108
+ """Health ساده برای تست سرویس."""
109
  return {
110
  "status": "online",
111
  "version": "1.0.0",
112
+ "timestamp": _utc_now().isoformat(),
113
  "ccxt_version": ccxt.__version__,
114
  }
115
 
116
 
117
  @app.get("/api/markets")
118
  async def get_markets():
119
+ """نمونه گرفتن لیست مارکت‌ها از Binance."""
120
  try:
121
  exchange = ccxt.binance()
122
  markets = exchange.load_markets()
123
  return {
124
  "success": True,
125
  "total_markets": len(markets),
126
+ "markets": list(markets.keys())[:50],
127
  }
128
  except Exception as e: # noqa: BLE001
129
  return {"success": False, "error": str(e)}
 
131
 
132
  @app.get("/api/ticker/{symbol}")
133
  async def get_ticker(symbol: str):
134
+ """نمونه گرفتن تیکر از Binance (مثال: BTC/USDT)."""
135
  try:
136
  exchange = ccxt.binance()
137
  ticker = exchange.fetch_ticker(symbol)
 
141
 
142
 
143
  # ------------------------------
144
+ # /api/status مطابق انتظار فرانت
 
 
 
 
145
  # ------------------------------
 
146
  @app.get("/api/status")
147
  async def api_status():
148
+ providers = _generate_providers_snapshot()
149
+ total = len(providers)
150
+ online = sum(1 for p in providers if p["status"] == "online")
151
+ offline = sum(1 for p in providers if p["status"] == "offline")
152
+ degraded = sum(1 for p in providers if p["status"] == "degraded")
153
+
154
+ avg_response = (
155
+ sum(p["response_time_ms"] for p in providers) / total if total else 0
156
+ )
157
+
158
+ # متریک‌های ساده
159
+ total_requests_hour = random.randint(200, 1200)
160
+ total_failures_hour = random.randint(0, max(20, degraded * 2 + offline * 5))
161
+
162
+ if offline == 0 and degraded <= max(1, total // 4):
163
+ system_health = "healthy"
164
+ else:
165
+ system_health = "degraded"
166
+
167
+ now = _utc_now()
168
  uptime_seconds = (now - START_TIME).total_seconds()
 
169
 
 
170
  return {
171
  "status": "online",
172
  "service": "crypto-data-source",
173
  "version": "1.0.0",
174
  "timestamp": now.isoformat(),
175
  "uptime_seconds": uptime_seconds,
176
+ "total_providers": total,
177
+ "online": online,
178
+ "offline": offline,
179
+ "degraded": degraded,
180
+ "avg_response_time_ms": int(avg_response),
181
+ "system_health": system_health,
182
+ "total_requests_hour": total_requests_hour,
183
+ "total_failures_hour": total_failures_hour,
 
 
 
 
184
  }
185
 
186
 
187
+ # ------------------------------
188
+ # /api/providers – لیست Providerها
189
+ # ------------------------------
190
+ @app.get("/api/providers")
191
+ async def api_providers():
192
  """
193
+ خروجی: آرایه‌ای از provider ها
194
+ [
195
+ {
196
+ name, category, status, response_time_ms,
197
+ last_fetch, has_key, priority
198
+ }, ...
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  ]
200
+ """
201
+ return _generate_providers_snapshot()
202
 
203
 
204
+ # ------------------------------
205
+ # /api/categories – خلاصه per category
206
+ # ------------------------------
207
+ @app.get("/api/categories")
208
+ async def api_categories():
 
209
  """
210
+ فرانت انتظار دارد آرایه مستقیم دریافت کند، نه شیء.
211
+
212
+ هر category: name, status, online_sources, total_sources, avg_response_time_ms
213
  """
214
+ providers = _generate_providers_snapshot()
215
+ by_cat: Dict[str, List[Dict[str, Any]]] = {}
216
+ for p in providers:
217
+ by_cat.setdefault(p["category"], []).append(p)
218
+
219
+ categories: List[Dict[str, Any]] = []
220
+ for category, items in by_cat.items():
221
+ total_sources = len(items)
222
+ online_sources = sum(1 for i in items if i["status"] == "online")
223
+ avg_rt = (
224
+ sum(i["response_time_ms"] for i in items) / total_sources
225
+ if total_sources
226
+ else 0
227
+ )
228
+ status = "online" if online_sources > 0 else "offline"
229
+ categories.append(
230
+ {
231
+ "name": category,
232
+ "status": status,
233
+ "online_sources": online_sources,
234
+ "total_sources": total_sources,
235
+ "avg_response_time_ms": int(avg_rt),
236
  }
237
+ )
238
 
239
+ return categories
240
 
241
 
242
+ # ------------------------------
243
+ # /api/charts/health-history – برای نمودار سلامت
244
+ # ------------------------------
245
  @app.get("/api/charts/health-history")
246
  async def api_chart_health_history(hours: int = 24):
247
  """
248
+ فرانت انتظار دارد:
249
+ { timestamps: [...], success_rate: [...] } // success_rate بین 0 تا 100
250
  """
251
+ now = _utc_now()
252
+ points = max(12, hours)
253
+ timestamps: List[str] = []
254
+ success_rate: List[float] = []
255
+
256
+ for i in range(points):
257
+ ts = now - timedelta(hours=(hours * (points - 1 - i) / points))
258
+ base = 90 # درصد پایه
259
+ noise = random.uniform(-10, 5)
260
+ value = max(50.0, min(100.0, base + noise))
261
+ timestamps.append(ts.isoformat())
262
+ success_rate.append(round(value, 2))
263
 
264
+ return {"timestamps": timestamps, "success_rate": success_rate}
265
 
266
+
267
+ # ------------------------------
268
+ # /api/charts/compliance – برای نمودار Bar
269
+ # ------------------------------
270
  @app.get("/api/charts/compliance")
271
  async def api_chart_compliance(days: int = 7):
272
  """
273
+ فرانت انتظار دارد:
274
+ { dates: [...], compliance_percentage: [...] } // 0..100
275
  """
276
+ today = _utc_now().date()
277
+ dates: List[str] = []
278
+ compliance_percentage: List[float] = []
279
+
280
+ for i in range(days):
281
+ d = today - timedelta(days=(days - 1 - i))
282
+ base = 88
283
+ noise = random.uniform(-8, 5)
284
+ value = max(60.0, min(100.0, base + noise))
285
+ dates.append(d.isoformat())
286
+ compliance_percentage.append(round(value, 2))
287
+
288
+ return {"dates": dates, "compliance_percentage": compliance_percentage}
289
+
290
+
291
+ # ------------------------------
292
+ # /api/freshness – جدول تازگی داده‌ها
293
+ # ------------------------------
294
+ @app.get("/api/freshness")
295
+ async def api_freshness():
296
+ """
297
+ فرانت انتظار دارد آرایه‌ای از آیتم‌ها:
298
+ provider, category, fetch_time, staleness_minutes, ttl_minutes, status
299
+ """
300
+ providers = _generate_providers_snapshot()
301
+ items: List[Dict[str, Any]] = []
302
+
303
+ for p in providers:
304
+ fetch_time = datetime.fromisoformat(p["last_fetch"])
305
+ staleness_minutes = (_utc_now() - fetch_time).total_seconds() / 60.0
306
+ ttl_minutes = 30
307
+ status = "fresh" if staleness_minutes <= ttl_minutes else "stale"
308
+ items.append(
309
+ {
310
+ "provider": p["name"],
311
+ "category": p["category"],
312
+ "fetch_time": fetch_time.isoformat(),
313
+ "staleness_minutes": round(staleness_minutes, 1),
314
+ "ttl_minutes": ttl_minutes,
315
+ "status": status,
316
+ }
317
+ )
318
+
319
+ return items
320
 
321
 
322
  # ------------------------------
323
+ # /api/logsلیست لاگ‌ها
324
  # ------------------------------
325
+ @app.get("/api/logs")
326
+ async def api_logs():
327
+ """
328
+ فرمت آیتم لاگ:
329
+ timestamp, provider, endpoint, status, http_code, response_time_ms, error_message?
330
+ """
331
+ providers = _generate_providers_snapshot()
332
+ logs: List[Dict[str, Any]] = []
333
+ now = _utc_now()
334
+
335
+ for p in providers:
336
+ # یک لاگ موفق
337
+ logs.append(
338
+ {
339
+ "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
340
+ "provider": p["name"],
341
+ "endpoint": "/api/status",
342
+ "status": "success",
343
+ "http_code": 200,
344
+ "response_time_ms": p["response_time_ms"],
345
+ "error_message": None,
346
+ }
347
+ )
348
 
349
+ # احتمالاً یک لاگ خطا
350
+ if p["status"] != "online" and random.random() < 0.5:
351
+ logs.append(
352
+ {
353
+ "timestamp": (
354
+ now - timedelta(minutes=random.randint(0, 60))
355
+ ).isoformat(),
356
+ "provider": p["name"],
357
+ "endpoint": "/api/markets",
358
+ "status": "error",
359
+ "http_code": random.choice([429, 500, 504]),
360
+ "response_time_ms": random.randint(500, 2000),
361
+ "error_message": "Upstream error or timeout",
362
+ }
363
+ )
364
+
365
+ # مرتب‌سازی نزولی بر اساس زمان
366
+ logs.sort(key=lambda x: x["timestamp"], reverse=True)
367
+ return logs
368
+
369
+
370
+ # ------------------------------
371
+ # /api/failures – خطاهای اخیر
372
+ # ------------------------------
373
+ @app.get("/api/failures")
374
+ async def api_failures():
375
+ """
376
+ خروجی:
377
+ { recent_failures: [ {timestamp, provider, error_type, error_message, retry_attempted}, ... ] }
378
+ """
379
+ logs = await api_logs()
380
+ failures: List[Dict[str, Any]] = []
381
+
382
+ for log in logs:
383
+ if log["status"] == "error":
384
+ failures.append(
385
+ {
386
+ "timestamp": log["timestamp"],
387
+ "provider": log["provider"],
388
+ "error_type": "HTTP " + str(log["http_code"]),
389
+ "error_message": log["error_message"] or "Unknown error",
390
+ "retry_attempted": random.random() < 0.6,
391
+ }
392
+ )
393
+
394
+ return {"recent_failures": failures}
395
+
396
+
397
+ # ------------------------------
398
+ # /api/config/keys – نمایش کلیدها
399
+ # ------------------------------
400
+ @app.get("/api/config/keys")
401
+ async def api_config_keys():
402
+ """
403
+ فرانت انتظار دارد آرایه‌ای از آیتم‌ها:
404
+ provider, key_masked
405
+ """
406
+ return API_KEYS
407
+
408
+
409
+ # ------------------------------
410
+ # /api/custom/add – افزودن API سفارشی
411
+ # ------------------------------
412
+ @app.post("/api/custom/add")
413
+ async def api_custom_add(
414
+ name: str = Query(...),
415
+ url: str = Query(...),
416
+ category: str = Query(...),
417
+ test_field: Optional[str] = Query(None),
418
+ ):
419
+ """
420
+ از روی فرانت با POST و query string صدا زده می‌شود.
421
+ در این نسخه، API را فقط در حافظه (CUSTOM_APIS) نگه می‌داریم.
422
+ """
423
+ CUSTOM_APIS.append(
424
+ {
425
+ "name": name,
426
+ "url": url,
427
+ "category": category or "other",
428
+ "test_field": test_field,
429
+ "has_key": False,
430
+ "priority": 3,
431
+ }
432
+ )
433
+
434
+ return {"success": True, "message": "Custom API added successfully"}
435
+
436
+
437
+ # ------------------------------
438
+ # WebSocket /ws/live – برای داشبورد
439
+ # ------------------------------
440
  class LiveConnections:
441
  def __init__(self) -> None:
442
  self.active: List[WebSocket] = []
 
454
  try:
455
  await ws.send_json(message)
456
  except Exception:
 
457
  self.disconnect(ws)
458
 
459
 
 
463
  @app.websocket("/ws/live")
464
  async def websocket_live(websocket: WebSocket):
465
  """
466
+ WebSocket ساده برای ارسال متریک‌های زنده.
 
467
  """
468
  await live_manager.connect(websocket)
469
  try:
 
470
  await websocket.send_json(
471
  {
472
  "type": "welcome",
473
  "service": "crypto-data-source",
474
+ "timestamp": _utc_now().isoformat(),
475
  }
476
  )
477
 
478
  while True:
 
479
  payload = {
480
  "type": "live_metrics",
481
+ "timestamp": _utc_now().isoformat(),
482
  "metrics": {
483
  "cpu_usage": random.randint(5, 40),
484
+ "memory_usage": random.randint(20, 75),
485
+ "active_requests": random.randint(1, 30),
486
  "error_rate_percent": round(random.uniform(0.0, 3.0), 2),
487
  },
488
  }
 
495
 
496
 
497
  # ------------------------------
498
+ # سرو کردن index.html و SPA routing
499
  # ------------------------------
 
500
  @app.get("/")
501
  async def root():
502
+ """اگر index.html کنار فایل باشد، همان را می‌فرستیم، در غیر این صورت JSON ساده."""
 
 
 
503
  if INDEX_FILE.exists():
504
  return FileResponse(INDEX_FILE)
505
  return JSONResponse(
506
  {
507
  "message": "No frontend index.html found in project root.",
508
+ "hint": "Place index.html next to this Python file or use /docs for the API.",
509
  "api_docs": "/docs",
510
  }
511
  )
 
514
  @app.get("/{full_path:path}")
515
  async def spa_catch_all(full_path: str):
516
  """
517
+ برای روتینگ سمت فرانت:
518
+ - مسیرهای /api/* اگر endpoint نداشته باشند: 404 JSON
519
+ - بقیه مسیرها: index.html (اگر باشد) یا 404
 
520
  """
 
521
  if full_path.startswith("api/"):
522
  return JSONResponse(
523
  {"error": "API endpoint not found", "path": full_path},
524
  status_code=404,
525
  )
526
 
527
+ # اگر فایل static با همین مسیر وجود دارد
528
  if STATIC_DIR.exists():
529
  candidate_file = STATIC_DIR / full_path
530
  if candidate_file.is_file():
531
  return FileResponse(candidate_file)
532
 
 
533
  if INDEX_FILE.exists():
534
  return FileResponse(INDEX_FILE)
535
 
 
536
  return JSONResponse(
537
  {"error": "No frontend available for this path.", "path": full_path},
538
  status_code=404,
 
540
 
541
 
542
  # ------------------------------
543
+ # اجرای لوکال
544
  # ------------------------------
545
  if __name__ == "__main__":
546
  import uvicorn