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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -157
app.py CHANGED
@@ -1,5 +1,4 @@
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
@@ -10,33 +9,26 @@ 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,
20
- allow_origins=["*"], # در صورت نیاز محدودترش کن
21
  allow_credentials=True,
22
  allow_methods=["*"],
23
  allow_headers=["*"],
24
  )
25
 
26
- # ------------------------------
27
- # مسیرهای پایه پروژه
28
- # ------------------------------
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},
@@ -61,27 +53,23 @@ 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
 
@@ -100,12 +88,11 @@ def _generate_providers_snapshot() -> List[Dict[str, Any]]:
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",
@@ -116,7 +103,6 @@ async def health_check():
116
 
117
  @app.get("/api/markets")
118
  async def get_markets():
119
- """نمونه گرفتن لیست مارکت‌ها از Binance."""
120
  try:
121
  exchange = ccxt.binance()
122
  markets = exchange.load_markets()
@@ -125,24 +111,23 @@ async def get_markets():
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)}
130
 
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)
138
  return {"success": True, "data": ticker}
139
- except Exception as e: # noqa: BLE001
140
  return {"success": False, "error": str(e)}
141
 
142
 
143
- # ------------------------------
144
- # /api/status – مطابق انتظار فرانت
145
- # ------------------------------
146
  @app.get("/api/status")
147
  async def api_status():
148
  providers = _generate_providers_snapshot()
@@ -150,19 +135,18 @@ async def api_status():
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()
@@ -184,33 +168,19 @@ async def api_status():
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:
@@ -239,15 +209,11 @@ async def api_categories():
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] = []
@@ -255,7 +221,7 @@ async def api_chart_health_history(hours: int = 24):
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())
@@ -264,15 +230,11 @@ async def api_chart_health_history(hours: int = 24):
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] = []
@@ -288,15 +250,11 @@ async def api_chart_compliance(days: int = 7):
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
 
@@ -319,21 +277,16 @@ async def api_freshness():
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(),
@@ -345,14 +298,10 @@ async def api_logs():
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",
@@ -362,20 +311,15 @@ async def api_logs():
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
 
@@ -394,21 +338,17 @@ async def api_failures():
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(...),
@@ -416,10 +356,6 @@ async def api_custom_add(
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,
@@ -430,13 +366,12 @@ async def api_custom_add(
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] = []
@@ -449,22 +384,12 @@ class LiveConnections:
449
  if websocket in self.active:
450
  self.active.remove(websocket)
451
 
452
- async def broadcast(self, message: Dict[str, Any]) -> None:
453
- for ws in list(self.active):
454
- try:
455
- await ws.send_json(message)
456
- except Exception:
457
- self.disconnect(ws)
458
-
459
 
460
  live_manager = LiveConnections()
461
 
462
 
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(
@@ -494,42 +419,31 @@ async def websocket_live(websocket: WebSocket):
494
  live_manager.disconnect(websocket)
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
  )
512
 
513
 
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
 
@@ -537,12 +451,3 @@ async def spa_catch_all(full_path: str):
537
  {"error": "No frontend available for this path.", "path": full_path},
538
  status_code=404,
539
  )
540
-
541
-
542
- # ------------------------------
543
- # اجرای لوکال
544
- # ------------------------------
545
- if __name__ == "__main__":
546
- import uvicorn
547
-
548
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
 
2
  from fastapi.responses import FileResponse, JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from pathlib import Path
 
9
  import random
10
  import ccxt
11
 
12
+ # -------------------------------------------------
13
+ # اپلیکیشن FastAPI
14
+ # -------------------------------------------------
15
  app = FastAPI(title="Crypto Data Source API")
16
 
17
+ # CORS آزاد (در صورت نیاز بعداً محدود کن)
 
 
18
  app.add_middleware(
19
  CORSMiddleware,
20
+ allow_origins=["*"],
21
  allow_credentials=True,
22
  allow_methods=["*"],
23
  allow_headers=["*"],
24
  )
25
 
26
+ # روت پروژه = همون جایی که app.py هست
 
 
27
  BASE_DIR = Path(__file__).resolve().parent
28
  INDEX_FILE = BASE_DIR / "index.html"
 
29
  START_TIME = datetime.now(timezone.utc)
30
 
31
+ # داده‌های نمونه برای داشبورد
 
 
 
 
 
32
  BASE_PROVIDERS: List[Dict[str, Any]] = [
33
  {"name": "Binance Spot", "category": "market_data", "has_key": False, "priority": 5},
34
  {"name": "Binance Futures", "category": "market_data", "has_key": False, "priority": 4},
 
53
  return datetime.now(timezone.utc)
54
 
55
 
 
 
 
56
  def _generate_providers_snapshot() -> List[Dict[str, Any]]:
57
+ """ساخت دیتای فیک برای provider ها (برای داشبورد)."""
58
  now = _utc_now()
59
  providers: List[Dict[str, Any]] = []
60
 
61
  all_providers = BASE_PROVIDERS + CUSTOM_APIS
62
 
63
  for base in all_providers:
64
+ rt = random.randint(80, 900) # response time
 
 
65
  minutes_ago = random.uniform(0, 30)
66
  last_fetch_dt = now - timedelta(minutes=minutes_ago)
67
 
 
68
  if rt < 300:
69
  status = "online"
70
  elif rt < 700:
71
  status = "degraded"
72
+ rt += random.randint(50, 200)
73
  else:
74
  status = "offline"
75
 
 
88
  return providers
89
 
90
 
91
+ # -------------------------------------------------
92
+ # APIهای ccxt پایه (health / markets / ticker)
93
+ # -------------------------------------------------
94
  @app.get("/api/health")
95
  async def health_check():
 
96
  return {
97
  "status": "online",
98
  "version": "1.0.0",
 
103
 
104
  @app.get("/api/markets")
105
  async def get_markets():
 
106
  try:
107
  exchange = ccxt.binance()
108
  markets = exchange.load_markets()
 
111
  "total_markets": len(markets),
112
  "markets": list(markets.keys())[:50],
113
  }
114
+ except Exception as e:
115
  return {"success": False, "error": str(e)}
116
 
117
 
118
  @app.get("/api/ticker/{symbol}")
119
  async def get_ticker(symbol: str):
 
120
  try:
121
  exchange = ccxt.binance()
122
  ticker = exchange.fetch_ticker(symbol)
123
  return {"success": True, "data": ticker}
124
+ except Exception as e:
125
  return {"success": False, "error": str(e)}
126
 
127
 
128
+ # -------------------------------------------------
129
+ # /api/status – کارت‌های بالای داشبورد
130
+ # -------------------------------------------------
131
  @app.get("/api/status")
132
  async def api_status():
133
  providers = _generate_providers_snapshot()
 
135
  online = sum(1 for p in providers if p["status"] == "online")
136
  offline = sum(1 for p in providers if p["status"] == "offline")
137
  degraded = sum(1 for p in providers if p["status"] == "degraded")
 
138
  avg_response = (
139
  sum(p["response_time_ms"] for p in providers) / total if total else 0
140
  )
141
 
 
142
  total_requests_hour = random.randint(200, 1200)
143
  total_failures_hour = random.randint(0, max(20, degraded * 2 + offline * 5))
144
 
145
+ system_health = (
146
+ "healthy"
147
+ if offline == 0 and degraded <= max(1, total // 4)
148
+ else "degraded"
149
+ )
150
 
151
  now = _utc_now()
152
  uptime_seconds = (now - START_TIME).total_seconds()
 
168
  }
169
 
170
 
171
+ # -------------------------------------------------
172
+ # /api/providers – جدول providerها
173
+ # -------------------------------------------------
174
  @app.get("/api/providers")
175
  async def api_providers():
 
 
 
 
 
 
 
 
 
176
  return _generate_providers_snapshot()
177
 
178
 
179
+ # -------------------------------------------------
180
+ # /api/categories – کارت‌های وسط
181
+ # -------------------------------------------------
182
  @app.get("/api/categories")
183
  async def api_categories():
 
 
 
 
 
184
  providers = _generate_providers_snapshot()
185
  by_cat: Dict[str, List[Dict[str, Any]]] = {}
186
  for p in providers:
 
209
  return categories
210
 
211
 
212
+ # -------------------------------------------------
213
+ # /api/charts/health-history – نمودار خطی
214
+ # -------------------------------------------------
215
  @app.get("/api/charts/health-history")
216
  async def api_chart_health_history(hours: int = 24):
 
 
 
 
217
  now = _utc_now()
218
  points = max(12, hours)
219
  timestamps: List[str] = []
 
221
 
222
  for i in range(points):
223
  ts = now - timedelta(hours=(hours * (points - 1 - i) / points))
224
+ base = 90
225
  noise = random.uniform(-10, 5)
226
  value = max(50.0, min(100.0, base + noise))
227
  timestamps.append(ts.isoformat())
 
230
  return {"timestamps": timestamps, "success_rate": success_rate}
231
 
232
 
233
+ # -------------------------------------------------
234
+ # /api/charts/compliance – نمودار میله‌ای
235
+ # -------------------------------------------------
236
  @app.get("/api/charts/compliance")
237
  async def api_chart_compliance(days: int = 7):
 
 
 
 
238
  today = _utc_now().date()
239
  dates: List[str] = []
240
  compliance_percentage: List[float] = []
 
250
  return {"dates": dates, "compliance_percentage": compliance_percentage}
251
 
252
 
253
+ # -------------------------------------------------
254
+ # /api/freshness – جدول تازگی
255
+ # -------------------------------------------------
256
  @app.get("/api/freshness")
257
  async def api_freshness():
 
 
 
 
258
  providers = _generate_providers_snapshot()
259
  items: List[Dict[str, Any]] = []
260
 
 
277
  return items
278
 
279
 
280
+ # -------------------------------------------------
281
+ # /api/logs – لاگ‌ها
282
+ # -------------------------------------------------
283
  @app.get("/api/logs")
284
  async def api_logs():
 
 
 
 
285
  providers = _generate_providers_snapshot()
286
  logs: List[Dict[str, Any]] = []
287
  now = _utc_now()
288
 
289
  for p in providers:
 
290
  logs.append(
291
  {
292
  "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
 
298
  "error_message": None,
299
  }
300
  )
 
 
301
  if p["status"] != "online" and random.random() < 0.5:
302
  logs.append(
303
  {
304
+ "timestamp": (now - timedelta(minutes=random.randint(0, 60))).isoformat(),
 
 
305
  "provider": p["name"],
306
  "endpoint": "/api/markets",
307
  "status": "error",
 
311
  }
312
  )
313
 
 
314
  logs.sort(key=lambda x: x["timestamp"], reverse=True)
315
  return logs
316
 
317
 
318
+ # -------------------------------------------------
319
+ # /api/failures – خطاها
320
+ # -------------------------------------------------
321
  @app.get("/api/failures")
322
  async def api_failures():
 
 
 
 
323
  logs = await api_logs()
324
  failures: List[Dict[str, Any]] = []
325
 
 
338
  return {"recent_failures": failures}
339
 
340
 
341
+ # -------------------------------------------------
342
+ # /api/config/keys – کلیدها
343
+ # -------------------------------------------------
344
  @app.get("/api/config/keys")
345
  async def api_config_keys():
 
 
 
 
346
  return API_KEYS
347
 
348
 
349
+ # -------------------------------------------------
350
+ # /api/custom/add – API سفارشی
351
+ # -------------------------------------------------
352
  @app.post("/api/custom/add")
353
  async def api_custom_add(
354
  name: str = Query(...),
 
356
  category: str = Query(...),
357
  test_field: Optional[str] = Query(None),
358
  ):
 
 
 
 
359
  CUSTOM_APIS.append(
360
  {
361
  "name": name,
 
366
  "priority": 3,
367
  }
368
  )
 
369
  return {"success": True, "message": "Custom API added successfully"}
370
 
371
 
372
+ # -------------------------------------------------
373
+ # WebSocket /ws/live
374
+ # -------------------------------------------------
375
  class LiveConnections:
376
  def __init__(self) -> None:
377
  self.active: List[WebSocket] = []
 
384
  if websocket in self.active:
385
  self.active.remove(websocket)
386
 
 
 
 
 
 
 
 
387
 
388
  live_manager = LiveConnections()
389
 
390
 
391
  @app.websocket("/ws/live")
392
  async def websocket_live(websocket: WebSocket):
 
 
 
393
  await live_manager.connect(websocket)
394
  try:
395
  await websocket.send_json(
 
419
  live_manager.disconnect(websocket)
420
 
421
 
422
+ # -------------------------------------------------
423
+ # سرو index.html از همون روت
424
+ # -------------------------------------------------
425
  @app.get("/")
426
  async def root():
 
427
  if INDEX_FILE.exists():
428
  return FileResponse(INDEX_FILE)
429
  return JSONResponse(
430
  {
431
+ "message": "index.html not found next to app.py.",
432
+ "hint": "Put index.html in the same folder as app.py or use /docs.",
 
433
  }
434
  )
435
 
436
 
437
  @app.get("/{full_path:path}")
438
  async def spa_catch_all(full_path: str):
439
+ # اگر مسیر /api/... باشه و endpoint تعریف نشده باشه
 
 
 
 
440
  if full_path.startswith("api/"):
441
  return JSONResponse(
442
  {"error": "API endpoint not found", "path": full_path},
443
  status_code=404,
444
  )
445
 
446
+ # غیر از API، همیشه همون index.html رو برگردون (SPA)
 
 
 
 
 
447
  if INDEX_FILE.exists():
448
  return FileResponse(INDEX_FILE)
449
 
 
451
  {"error": "No frontend available for this path.", "path": full_path},
452
  status_code=404,
453
  )