Really-amin commited on
Commit
f240312
·
verified ·
1 Parent(s): e22b4a0

Upload 3 files

Browse files
templates/README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crypto Data Source API (Dual Mode)
2
+
3
+ **Updated:** 2025-11-11T20:56:45.223511Z
4
+
5
+ This backend is designed to support **both** frontend shapes you used:
6
+ - Classic `/api/*` endpoints (status/providers/categories/charts/logs/failures/freshness/config/keys/custom/add).
7
+ - Alternate aliases and extras that some UIs expect: `/health`, `/info`, `/api/rate-limits`, `/api/alerts`, `/api/logs?type=...`,
8
+ plus synthetic **Hugging Face** helpers under `/api/hf/*`.
9
+
10
+ ## Files
11
+ - `app.py` — FastAPI application (single file, no `frontend/` folder needed). Place your `index.html` next to it if you want static SPA routing.
12
+ - `requirements.txt` — minimal, conflict-free pins tested for HF Spaces.
13
+
14
+ ## Run (local)
15
+ ```bash
16
+ pip install -r requirements.txt
17
+ uvicorn app:app --host 0.0.0.0 --port 7860
18
+ ```
19
+
20
+ ## Endpoints
21
+ - `/` → serves `index.html` if present; else JSON hint
22
+ - `/ws/live` → live events (`live_metrics`, `status_update`, `provider_status_change`, `new_alert`)
23
+ - `/health` → alias of `/api/health`
24
+ - `/info` → status-like payload
25
+ - `/api/health`
26
+ - `/api/status`
27
+ - `/api/providers`
28
+ - `/api/categories`
29
+ - `/api/charts/health-history?hours=24`
30
+ - `/api/charts/compliance?days=7`
31
+ - `/api/freshness`
32
+ - `/api/logs` (optional `?type=connection|error`)
33
+ - `/api/failures` (alias exposed also at `/api/alerts`)
34
+ - `/api/rate-limits`
35
+ - `/api/config/keys`
36
+ - `/api/custom/add` (POST query params: `name`, `url`, `category`, optional `test_field`)
37
+ - `/api/markets`, `/api/ticker/{symbol}` (via ccxt)
38
+ - `/api/hf/health`, `/api/hf/refresh` (POST), `/api/hf/registry?type=models|datasets`, `/api/hf/search?q=&kind=`, `/api/hf/run-sentiment` (POST JSON: `{"text": "..."}`)
39
+
40
+ All non-critical data are **synthetic** to keep it lightweight on Spaces.
templates/app.py ADDED
@@ -0,0 +1,529 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)
templates/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ pydantic==2.5.0
4
+ python-multipart==0.0.6
5
+ websockets==11.0.3
6
+ ccxt==4.3.59
7
+ requests==2.31.0
8
+ httpx>=0.24