Really-amin commited on
Commit
c66011b
·
verified ·
1 Parent(s): 7115ee6

Upload api_server_extended.py

Browse files
Files changed (1) hide show
  1. api_server_extended.py +698 -734
api_server_extended.py CHANGED
@@ -1,734 +1,698 @@
1
- #!/usr/bin/env python3
2
- """
3
- API Server Extended - HuggingFace Spaces Deployment Ready
4
- Complete Admin API with Real Data Only - NO MOCKS
5
- """
6
-
7
- import os
8
- import asyncio
9
- import sqlite3
10
- import httpx
11
- import json
12
- import subprocess
13
- from pathlib import Path
14
- from typing import Optional, Dict, Any, List
15
- from datetime import datetime
16
- from contextlib import asynccontextmanager
17
- from collections import defaultdict
18
-
19
- from fastapi import FastAPI, HTTPException
20
- from fastapi.middleware.cors import CORSMiddleware
21
- from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
22
- from fastapi.staticfiles import StaticFiles
23
- from pydantic import BaseModel
24
-
25
- # Environment variables
26
- USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "false").lower() == "true"
27
- PORT = int(os.getenv("PORT", "7860"))
28
-
29
- # Paths
30
- WORKSPACE_ROOT = Path("/workspace" if Path("/workspace").exists() else ".")
31
- DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
32
- LOG_DIR = WORKSPACE_ROOT / "logs"
33
- PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
34
- APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
35
-
36
- # Ensure directories exist
37
- DB_PATH.parent.mkdir(parents=True, exist_ok=True)
38
- LOG_DIR.mkdir(parents=True, exist_ok=True)
39
-
40
- # Global state for providers
41
- _provider_state = {
42
- "providers": {},
43
- "pools": {},
44
- "logs": [],
45
- "alerts": [],
46
- "last_check": None,
47
- "stats": {"total": 0, "online": 0, "offline": 0, "degraded": 0}
48
- }
49
-
50
-
51
- # ===== Database Setup =====
52
- def init_database():
53
- """Initialize SQLite database with required tables"""
54
- conn = sqlite3.connect(str(DB_PATH))
55
- cursor = conn.cursor()
56
-
57
- cursor.execute("""
58
- CREATE TABLE IF NOT EXISTS prices (
59
- id INTEGER PRIMARY KEY AUTOINCREMENT,
60
- symbol TEXT NOT NULL,
61
- name TEXT,
62
- price_usd REAL NOT NULL,
63
- volume_24h REAL,
64
- market_cap REAL,
65
- percent_change_24h REAL,
66
- rank INTEGER,
67
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
68
- )
69
- """)
70
-
71
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
72
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
73
-
74
- conn.commit()
75
- conn.close()
76
- print(f"✓ Database initialized at {DB_PATH}")
77
-
78
-
79
- def save_price_to_db(price_data: Dict[str, Any]):
80
- """Save price data to SQLite"""
81
- try:
82
- conn = sqlite3.connect(str(DB_PATH))
83
- cursor = conn.cursor()
84
- cursor.execute("""
85
- INSERT INTO prices (symbol, name, price_usd, volume_24h, market_cap, percent_change_24h, rank)
86
- VALUES (?, ?, ?, ?, ?, ?, ?)
87
- """, (
88
- price_data.get("symbol"),
89
- price_data.get("name"),
90
- price_data.get("price_usd", 0.0),
91
- price_data.get("volume_24h"),
92
- price_data.get("market_cap"),
93
- price_data.get("percent_change_24h"),
94
- price_data.get("rank")
95
- ))
96
- conn.commit()
97
- conn.close()
98
- except Exception as e:
99
- print(f"Error saving price to database: {e}")
100
-
101
-
102
- def get_price_history_from_db(symbol: str, limit: int = 10) -> List[Dict[str, Any]]:
103
- """Get price history from SQLite"""
104
- try:
105
- conn = sqlite3.connect(str(DB_PATH))
106
- conn.row_factory = sqlite3.Row
107
- cursor = conn.cursor()
108
- cursor.execute("""
109
- SELECT * FROM prices
110
- WHERE symbol = ?
111
- ORDER BY timestamp DESC
112
- LIMIT ?
113
- """, (symbol, limit))
114
- rows = cursor.fetchall()
115
- conn.close()
116
- return [dict(row) for row in rows]
117
- except Exception as e:
118
- print(f"Error fetching price history: {e}")
119
- return []
120
-
121
-
122
- # ===== Provider Management =====
123
- def load_providers_config() -> Dict[str, Any]:
124
- """Load providers from config file"""
125
- try:
126
- if PROVIDERS_CONFIG_PATH.exists():
127
- with open(PROVIDERS_CONFIG_PATH, 'r') as f:
128
- return json.load(f)
129
- return {"providers": {}}
130
- except Exception as e:
131
- print(f"Error loading providers config: {e}")
132
- return {"providers": {}}
133
-
134
-
135
- def load_apl_report() -> Dict[str, Any]:
136
- """Load APL validation report"""
137
- try:
138
- if APL_REPORT_PATH.exists():
139
- with open(APL_REPORT_PATH, 'r') as f:
140
- return json.load(f)
141
- return {}
142
- except Exception as e:
143
- print(f"Error loading APL report: {e}")
144
- return {}
145
-
146
-
147
- # ===== Real Data Providers =====
148
- HEADERS = {
149
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
150
- "Accept": "application/json"
151
- }
152
-
153
-
154
- async def fetch_coingecko_simple_price() -> Dict[str, Any]:
155
- """Fetch real price data from CoinGecko API"""
156
- url = "https://api.coingecko.com/api/v3/simple/price"
157
- params = {
158
- "ids": "bitcoin,ethereum,binancecoin",
159
- "vs_currencies": "usd",
160
- "include_market_cap": "true",
161
- "include_24hr_vol": "true",
162
- "include_24hr_change": "true"
163
- }
164
-
165
- async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
166
- response = await client.get(url, params=params)
167
- if response.status_code != 200:
168
- raise HTTPException(status_code=503, detail=f"CoinGecko API error: HTTP {response.status_code}")
169
- return response.json()
170
-
171
-
172
- async def fetch_fear_greed_index() -> Dict[str, Any]:
173
- """Fetch real Fear & Greed Index from Alternative.me"""
174
- url = "https://api.alternative.me/fng/"
175
- params = {"limit": "1", "format": "json"}
176
-
177
- async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
178
- response = await client.get(url, params=params)
179
- if response.status_code != 200:
180
- raise HTTPException(status_code=503, detail=f"Alternative.me API error: HTTP {response.status_code}")
181
- return response.json()
182
-
183
-
184
- async def fetch_coingecko_trending() -> Dict[str, Any]:
185
- """Fetch real trending coins from CoinGecko"""
186
- url = "https://api.coingecko.com/api/v3/search/trending"
187
-
188
- async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
189
- response = await client.get(url)
190
- if response.status_code != 200:
191
- raise HTTPException(status_code=503, detail=f"CoinGecko trending API error: HTTP {response.status_code}")
192
- return response.json()
193
-
194
-
195
- # ===== Lifespan Management =====
196
- @asynccontextmanager
197
- async def lifespan(app: FastAPI):
198
- """Application lifespan manager"""
199
- print("=" * 80)
200
- print("🚀 Starting Crypto Monitor Admin API")
201
- print("=" * 80)
202
- init_database()
203
-
204
- # Load providers
205
- config = load_providers_config()
206
- _provider_state["providers"] = config.get("providers", {})
207
- print(f"✓ Loaded {len(_provider_state['providers'])} providers from config")
208
-
209
- # Load APL report
210
- apl_report = load_apl_report()
211
- if apl_report:
212
- print(f"✓ Loaded APL report with validation data")
213
-
214
- print(f" Server ready on port {PORT}")
215
- print("=" * 80)
216
- yield
217
- print("Shutting down...")
218
-
219
-
220
- # ===== FastAPI Application =====
221
- app = FastAPI(
222
- title="Crypto Monitor Admin API",
223
- description="Real-time cryptocurrency data API with Admin Dashboard",
224
- version="5.0.0",
225
- lifespan=lifespan
226
- )
227
-
228
- # CORS Middleware
229
- app.add_middleware(
230
- CORSMiddleware,
231
- allow_origins=["*"],
232
- allow_credentials=True,
233
- allow_methods=["*"],
234
- allow_headers=["*"],
235
- )
236
-
237
- # Mount static files
238
- try:
239
- static_path = WORKSPACE_ROOT / "static"
240
- if static_path.exists():
241
- app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
242
- print(f"✓ Mounted static files from {static_path}")
243
- except Exception as e:
244
- print(f"⚠ Could not mount static files: {e}")
245
-
246
-
247
- # ===== HTML UI Endpoints =====
248
- @app.get("/", response_class=HTMLResponse)
249
- async def serve_admin_dashboard():
250
- """Serve admin dashboard"""
251
- html_path = WORKSPACE_ROOT / "admin.html"
252
- if html_path.exists():
253
- return FileResponse(html_path)
254
- return HTMLResponse("<h1>Admin Dashboard</h1><p>admin.html not found</p>")
255
-
256
-
257
- # ===== Health & Status Endpoints =====
258
- @app.get("/health")
259
- async def health():
260
- """Health check endpoint"""
261
- return {
262
- "status": "healthy",
263
- "timestamp": datetime.now().isoformat(),
264
- "database": str(DB_PATH),
265
- "use_mock_data": USE_MOCK_DATA,
266
- "providers_loaded": len(_provider_state["providers"])
267
- }
268
-
269
-
270
- @app.get("/api/status")
271
- async def get_status():
272
- """System status"""
273
- config = load_providers_config()
274
- providers = config.get("providers", {})
275
-
276
- # Count by validation status
277
- validated_count = sum(1 for p in providers.values() if p.get("validated"))
278
-
279
- return {
280
- "system_health": "healthy",
281
- "timestamp": datetime.now().isoformat(),
282
- "total_providers": len(providers),
283
- "validated_providers": validated_count,
284
- "database_status": "connected",
285
- "apl_available": APL_REPORT_PATH.exists(),
286
- "use_mock_data": USE_MOCK_DATA
287
- }
288
-
289
-
290
- @app.get("/api/stats")
291
- async def get_stats():
292
- """System statistics"""
293
- config = load_providers_config()
294
- providers = config.get("providers", {})
295
-
296
- # Group by category
297
- categories = defaultdict(int)
298
- for p in providers.values():
299
- cat = p.get("category", "unknown")
300
- categories[cat] += 1
301
-
302
- return {
303
- "total_providers": len(providers),
304
- "categories": dict(categories),
305
- "total_categories": len(categories),
306
- "timestamp": datetime.now().isoformat()
307
- }
308
-
309
-
310
- # ===== Market Data Endpoint =====
311
- @app.get("/api/market")
312
- async def get_market_data():
313
- """Market data from CoinGecko - REAL DATA ONLY"""
314
- try:
315
- data = await fetch_coingecko_simple_price()
316
-
317
- cryptocurrencies = []
318
- coin_mapping = {
319
- "bitcoin": {"name": "Bitcoin", "symbol": "BTC", "rank": 1, "image": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"},
320
- "ethereum": {"name": "Ethereum", "symbol": "ETH", "rank": 2, "image": "https://assets.coingecko.com/coins/images/279/small/ethereum.png"},
321
- "binancecoin": {"name": "BNB", "symbol": "BNB", "rank": 3, "image": "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png"}
322
- }
323
-
324
- for coin_id, coin_info in coin_mapping.items():
325
- if coin_id in data:
326
- coin_data = data[coin_id]
327
- crypto_entry = {
328
- "rank": coin_info["rank"],
329
- "name": coin_info["name"],
330
- "symbol": coin_info["symbol"],
331
- "price": coin_data.get("usd", 0),
332
- "change_24h": coin_data.get("usd_24h_change", 0),
333
- "market_cap": coin_data.get("usd_market_cap", 0),
334
- "volume_24h": coin_data.get("usd_24h_vol", 0),
335
- "image": coin_info["image"]
336
- }
337
- cryptocurrencies.append(crypto_entry)
338
-
339
- # Save to database
340
- save_price_to_db({
341
- "symbol": coin_info["symbol"],
342
- "name": coin_info["name"],
343
- "price_usd": crypto_entry["price"],
344
- "volume_24h": crypto_entry["volume_24h"],
345
- "market_cap": crypto_entry["market_cap"],
346
- "percent_change_24h": crypto_entry["change_24h"],
347
- "rank": coin_info["rank"]
348
- })
349
-
350
- # Calculate dominance
351
- total_market_cap = sum(c["market_cap"] for c in cryptocurrencies)
352
- btc_dominance = 0
353
- if total_market_cap > 0:
354
- btc_entry = next((c for c in cryptocurrencies if c["symbol"] == "BTC"), None)
355
- if btc_entry:
356
- btc_dominance = (btc_entry["market_cap"] / total_market_cap) * 100
357
-
358
- return {
359
- "cryptocurrencies": cryptocurrencies,
360
- "total_market_cap": total_market_cap,
361
- "btc_dominance": btc_dominance,
362
- "timestamp": datetime.now().isoformat(),
363
- "source": "CoinGecko API (Real Data)"
364
- }
365
-
366
- except Exception as e:
367
- raise HTTPException(status_code=503, detail=f"Failed to fetch market data: {str(e)}")
368
-
369
-
370
- @app.get("/api/market/history")
371
- async def get_market_history(symbol: str = "BTC", limit: int = 10):
372
- """Get price history from database - REAL DATA ONLY"""
373
- history = get_price_history_from_db(symbol.upper(), limit)
374
-
375
- if not history:
376
- return {
377
- "symbol": symbol,
378
- "history": [],
379
- "count": 0,
380
- "message": "No history available"
381
- }
382
-
383
- return {
384
- "symbol": symbol,
385
- "history": history,
386
- "count": len(history),
387
- "source": "SQLite Database (Real Data)"
388
- }
389
-
390
-
391
- @app.get("/api/sentiment")
392
- async def get_sentiment():
393
- """Sentiment data from Alternative.me - REAL DATA ONLY"""
394
- try:
395
- data = await fetch_fear_greed_index()
396
-
397
- if "data" in data and len(data["data"]) > 0:
398
- fng_data = data["data"][0]
399
- return {
400
- "fear_greed_index": int(fng_data["value"]),
401
- "fear_greed_label": fng_data["value_classification"],
402
- "timestamp": datetime.now().isoformat(),
403
- "source": "Alternative.me API (Real Data)"
404
- }
405
-
406
- raise HTTPException(status_code=503, detail="Invalid response from Alternative.me")
407
-
408
- except Exception as e:
409
- raise HTTPException(status_code=503, detail=f"Failed to fetch sentiment: {str(e)}")
410
-
411
-
412
- @app.get("/api/trending")
413
- async def get_trending():
414
- """Trending coins from CoinGecko - REAL DATA ONLY"""
415
- try:
416
- data = await fetch_coingecko_trending()
417
-
418
- trending_coins = []
419
- if "coins" in data:
420
- for item in data["coins"][:10]:
421
- coin = item.get("item", {})
422
- trending_coins.append({
423
- "id": coin.get("id"),
424
- "name": coin.get("name"),
425
- "symbol": coin.get("symbol"),
426
- "market_cap_rank": coin.get("market_cap_rank"),
427
- "thumb": coin.get("thumb"),
428
- "score": coin.get("score", 0)
429
- })
430
-
431
- return {
432
- "trending": trending_coins,
433
- "count": len(trending_coins),
434
- "timestamp": datetime.now().isoformat(),
435
- "source": "CoinGecko API (Real Data)"
436
- }
437
-
438
- except Exception as e:
439
- raise HTTPException(status_code=503, detail=f"Failed to fetch trending: {str(e)}")
440
-
441
-
442
- # ===== Providers Management Endpoints =====
443
- @app.get("/api/providers")
444
- async def get_providers():
445
- """Get all providers - REAL DATA from config"""
446
- config = load_providers_config()
447
- providers = config.get("providers", {})
448
-
449
- result = []
450
- for provider_id, provider_data in providers.items():
451
- result.append({
452
- "provider_id": provider_id,
453
- "name": provider_data.get("name", provider_id),
454
- "category": provider_data.get("category", "unknown"),
455
- "type": provider_data.get("type", "unknown"),
456
- "status": "validated" if provider_data.get("validated") else "unvalidated",
457
- "validated_at": provider_data.get("validated_at"),
458
- "response_time_ms": provider_data.get("response_time_ms"),
459
- "added_by": provider_data.get("added_by", "manual")
460
- })
461
-
462
- return {
463
- "providers": result,
464
- "total": len(result),
465
- "source": "providers_config_extended.json (Real Data)"
466
- }
467
-
468
- @app.get("/api/providers/config")
469
- async def get_providers_config():
470
- """Return raw providers configuration file"""
471
- config = load_providers_config()
472
- return config
473
-
474
-
475
-
476
- @app.get("/api/providers/{provider_id}")
477
- async def get_provider_detail(provider_id: str):
478
- """Get specific provider details"""
479
- config = load_providers_config()
480
- providers = config.get("providers", {})
481
-
482
- if provider_id not in providers:
483
- raise HTTPException(status_code=404, detail=f"Provider {provider_id} not found")
484
-
485
- return {
486
- "provider_id": provider_id,
487
- **providers[provider_id]
488
- }
489
-
490
-
491
- @app.get("/api/providers/category/{category}")
492
- async def get_providers_by_category(category: str):
493
- """Get providers by category"""
494
- config = load_providers_config()
495
- providers = config.get("providers", {})
496
-
497
- filtered = {
498
- pid: data for pid, data in providers.items()
499
- if data.get("category") == category
500
- }
501
-
502
- return {
503
- "category": category,
504
- "providers": filtered,
505
- "count": len(filtered)
506
- }
507
-
508
-
509
- # ===== Pools Endpoints (Placeholder - to be implemented) =====
510
- @app.get("/api/pools")
511
- async def get_pools():
512
- """Get provider pools"""
513
- return {
514
- "pools": [],
515
- "message": "Pools feature not yet implemented in this version"
516
- }
517
-
518
-
519
- # ===== Logs Endpoints =====
520
-
521
-
522
- @app.get("/api/logs")
523
- async def get_logs(limit: int = 50):
524
- """
525
- Get recent logs with an optional limit parameter for the admin UI.
526
- """
527
- all_logs = _provider_state.get("logs", [])
528
- if limit <= 0:
529
- limit = 50
530
- sliced = all_logs[-limit:]
531
- return {
532
- "logs": sliced,
533
- "count": len(sliced),
534
- }
535
-
536
-
537
- @app.get("/api/alerts")
538
- async def get_alerts():
539
- """
540
- Get system alerts for the admin UI.
541
- """
542
- alerts = _provider_state.get("alerts", [])
543
- return {
544
- "alerts": alerts,
545
- "count": len(alerts),
546
- "timestamp": datetime.now().isoformat(),
547
- }
548
- @app.get("/api/logs/recent")
549
- async def get_recent_logs():
550
- """Get recent logs"""
551
- return {
552
- "logs": _provider_state.get("logs", [])[-50:],
553
- "count": min(50, len(_provider_state.get("logs", [])))
554
- }
555
-
556
-
557
- @app.get("/api/logs/errors")
558
- async def get_error_logs():
559
- """Get error logs"""
560
- all_logs = _provider_state.get("logs", [])
561
- errors = [log for log in all_logs if log.get("level") == "ERROR"]
562
- return {
563
- "errors": errors[-50:],
564
- "count": len(errors)
565
- }
566
-
567
-
568
- # ===== Diagnostics Endpoints =====
569
- @app.post("/api/diagnostics/run")
570
- async def run_diagnostics(auto_fix: bool = False):
571
- """Run system diagnostics"""
572
- issues = []
573
- fixes_applied = []
574
-
575
- # Check database
576
- if not DB_PATH.exists():
577
- issues.append({"type": "database", "message": "Database file not found"})
578
- if auto_fix:
579
- init_database()
580
- fixes_applied.append("Initialized database")
581
-
582
- # Check providers config
583
- if not PROVIDERS_CONFIG_PATH.exists():
584
- issues.append({"type": "config", "message": "Providers config not found"})
585
-
586
- # Check APL report
587
- if not APL_REPORT_PATH.exists():
588
- issues.append({"type": "apl", "message": "APL report not found"})
589
-
590
- return {
591
- "status": "completed",
592
- "issues_found": len(issues),
593
- "issues": issues,
594
- "fixes_applied": fixes_applied if auto_fix else [],
595
- "timestamp": datetime.now().isoformat()
596
- }
597
-
598
-
599
- @app.get("/api/diagnostics/last")
600
- async def get_last_diagnostics():
601
- """Get last diagnostics results"""
602
- # Would load from file in real implementation
603
- return {
604
- "status": "no_previous_run",
605
- "message": "No previous diagnostics run found"
606
- }
607
-
608
-
609
- # ===== APL (Auto Provider Loader) Endpoints =====
610
- @app.post("/api/apl/run")
611
- async def run_apl_scan():
612
- """Run APL provider scan"""
613
- try:
614
- # Run APL script
615
- result = subprocess.run(
616
- ["python3", str(WORKSPACE_ROOT / "auto_provider_loader.py")],
617
- capture_output=True,
618
- text=True,
619
- timeout=300,
620
- cwd=str(WORKSPACE_ROOT)
621
- )
622
-
623
- # Reload providers after APL run
624
- config = load_providers_config()
625
- _provider_state["providers"] = config.get("providers", {})
626
-
627
- return {
628
- "status": "completed",
629
- "stdout": result.stdout[-1000:], # Last 1000 chars
630
- "returncode": result.returncode,
631
- "providers_count": len(_provider_state["providers"]),
632
- "timestamp": datetime.now().isoformat()
633
- }
634
-
635
- except subprocess.TimeoutExpired:
636
- return {
637
- "status": "timeout",
638
- "message": "APL scan timed out after 5 minutes"
639
- }
640
- except Exception as e:
641
- raise HTTPException(status_code=500, detail=f"APL scan failed: {str(e)}")
642
-
643
-
644
- @app.get("/api/apl/report")
645
- async def get_apl_report():
646
- """Get APL validation report"""
647
- report = load_apl_report()
648
-
649
- if not report:
650
- return {
651
- "status": "not_available",
652
- "message": "APL report not found. Run APL scan first."
653
- }
654
-
655
- return report
656
-
657
-
658
- @app.get("/api/apl/summary")
659
- async def get_apl_summary():
660
- """Get APL summary statistics"""
661
- report = load_apl_report()
662
-
663
- if not report or "stats" not in report:
664
- return {
665
- "status": "not_available",
666
- "message": "APL report not found"
667
- }
668
-
669
- stats = report.get("stats", {})
670
- return {
671
- "http_candidates": stats.get("total_http_candidates", 0),
672
- "http_valid": stats.get("http_valid", 0),
673
- "http_invalid": stats.get("http_invalid", 0),
674
- "http_conditional": stats.get("http_conditional", 0),
675
- "hf_candidates": stats.get("total_hf_candidates", 0),
676
- "hf_valid": stats.get("hf_valid", 0),
677
- "hf_invalid": stats.get("hf_invalid", 0),
678
- "hf_conditional": stats.get("hf_conditional", 0),
679
- "total_active": stats.get("total_active_providers", 0),
680
- "timestamp": stats.get("timestamp", "")
681
- }
682
-
683
-
684
- # ===== HF Models Endpoints =====
685
- @app.get("/api/hf/models")
686
- async def get_hf_models():
687
- """Get HuggingFace models from APL report"""
688
- report = load_apl_report()
689
-
690
- if not report:
691
- return {"models": [], "count": 0}
692
-
693
- hf_models = report.get("hf_models", {}).get("results", [])
694
-
695
- return {
696
- "models": hf_models,
697
- "count": len(hf_models),
698
- "source": "APL Validation Report (Real Data)"
699
- }
700
-
701
-
702
- @app.get("/api/hf/health")
703
- async def get_hf_health():
704
- """Get HF services health"""
705
- try:
706
- from backend.services.hf_registry import REGISTRY
707
- health = REGISTRY.health()
708
- return health
709
- except Exception as e:
710
- return {
711
- "ok": False,
712
- "error": f"HF registry not available: {str(e)}"
713
- }
714
-
715
-
716
- # ===== DeFi Endpoint - NOT IMPLEMENTED =====
717
- @app.get("/api/defi")
718
- async def get_defi():
719
- """DeFi endpoint - Not implemented"""
720
- raise HTTPException(status_code=503, detail="DeFi endpoint not implemented. Real data only - no fakes.")
721
-
722
-
723
- # ===== HuggingFace ML Sentiment - NOT IMPLEMENTED =====
724
- @app.post("/api/hf/run-sentiment")
725
- async def run_sentiment(data: Dict[str, Any]):
726
- """ML sentiment analysis - Not implemented"""
727
- raise HTTPException(status_code=501, detail="ML sentiment not implemented. Real data only - no fakes.")
728
-
729
-
730
- # ===== Main Entry Point =====
731
- if __name__ == "__main__":
732
- import uvicorn
733
- print(f"Starting Crypto Monitor Admin Server on port {PORT}")
734
- uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ API Server Extended - HuggingFace Spaces Deployment Ready
4
+ Complete Admin API with Real Data Only - NO MOCKS
5
+ """
6
+
7
+ import os
8
+ import asyncio
9
+ import sqlite3
10
+ import httpx
11
+ import json
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Optional, Dict, Any, List
15
+ from datetime import datetime
16
+ from contextlib import asynccontextmanager
17
+ from collections import defaultdict
18
+
19
+ from fastapi import FastAPI, HTTPException
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fastapi.responses import JSONResponse, FileResponse, HTMLResponse
22
+ from fastapi.staticfiles import StaticFiles
23
+ from pydantic import BaseModel
24
+
25
+ # Environment variables
26
+ USE_MOCK_DATA = os.getenv("USE_MOCK_DATA", "false").lower() == "true"
27
+ PORT = int(os.getenv("PORT", "7860"))
28
+
29
+ # Paths
30
+ WORKSPACE_ROOT = Path("/workspace" if Path("/workspace").exists() else ".")
31
+ DB_PATH = WORKSPACE_ROOT / "data" / "database" / "crypto_monitor.db"
32
+ LOG_DIR = WORKSPACE_ROOT / "logs"
33
+ PROVIDERS_CONFIG_PATH = WORKSPACE_ROOT / "providers_config_extended.json"
34
+ APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
35
+
36
+ # Ensure directories exist
37
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
38
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
39
+
40
+ # Global state for providers
41
+ _provider_state = {
42
+ "providers": {},
43
+ "pools": {},
44
+ "logs": [],
45
+ "last_check": None,
46
+ "stats": {"total": 0, "online": 0, "offline": 0, "degraded": 0}
47
+ }
48
+
49
+
50
+ # ===== Database Setup =====
51
+ def init_database():
52
+ """Initialize SQLite database with required tables"""
53
+ conn = sqlite3.connect(str(DB_PATH))
54
+ cursor = conn.cursor()
55
+
56
+ cursor.execute("""
57
+ CREATE TABLE IF NOT EXISTS prices (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ symbol TEXT NOT NULL,
60
+ name TEXT,
61
+ price_usd REAL NOT NULL,
62
+ volume_24h REAL,
63
+ market_cap REAL,
64
+ percent_change_24h REAL,
65
+ rank INTEGER,
66
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
67
+ )
68
+ """)
69
+
70
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
71
+ cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
72
+
73
+ conn.commit()
74
+ conn.close()
75
+ print(f"✓ Database initialized at {DB_PATH}")
76
+
77
+
78
+ def save_price_to_db(price_data: Dict[str, Any]):
79
+ """Save price data to SQLite"""
80
+ try:
81
+ conn = sqlite3.connect(str(DB_PATH))
82
+ cursor = conn.cursor()
83
+ cursor.execute("""
84
+ INSERT INTO prices (symbol, name, price_usd, volume_24h, market_cap, percent_change_24h, rank)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?)
86
+ """, (
87
+ price_data.get("symbol"),
88
+ price_data.get("name"),
89
+ price_data.get("price_usd", 0.0),
90
+ price_data.get("volume_24h"),
91
+ price_data.get("market_cap"),
92
+ price_data.get("percent_change_24h"),
93
+ price_data.get("rank")
94
+ ))
95
+ conn.commit()
96
+ conn.close()
97
+ except Exception as e:
98
+ print(f"Error saving price to database: {e}")
99
+
100
+
101
+ def get_price_history_from_db(symbol: str, limit: int = 10) -> List[Dict[str, Any]]:
102
+ """Get price history from SQLite"""
103
+ try:
104
+ conn = sqlite3.connect(str(DB_PATH))
105
+ conn.row_factory = sqlite3.Row
106
+ cursor = conn.cursor()
107
+ cursor.execute("""
108
+ SELECT * FROM prices
109
+ WHERE symbol = ?
110
+ ORDER BY timestamp DESC
111
+ LIMIT ?
112
+ """, (symbol, limit))
113
+ rows = cursor.fetchall()
114
+ conn.close()
115
+ return [dict(row) for row in rows]
116
+ except Exception as e:
117
+ print(f"Error fetching price history: {e}")
118
+ return []
119
+
120
+
121
+ # ===== Provider Management =====
122
+ def load_providers_config() -> Dict[str, Any]:
123
+ """Load providers from config file"""
124
+ try:
125
+ if PROVIDERS_CONFIG_PATH.exists():
126
+ with open(PROVIDERS_CONFIG_PATH, 'r') as f:
127
+ return json.load(f)
128
+ return {"providers": {}}
129
+ except Exception as e:
130
+ print(f"Error loading providers config: {e}")
131
+ return {"providers": {}}
132
+
133
+
134
+ def load_apl_report() -> Dict[str, Any]:
135
+ """Load APL validation report"""
136
+ try:
137
+ if APL_REPORT_PATH.exists():
138
+ with open(APL_REPORT_PATH, 'r') as f:
139
+ return json.load(f)
140
+ return {}
141
+ except Exception as e:
142
+ print(f"Error loading APL report: {e}")
143
+ return {}
144
+
145
+
146
+ # ===== Real Data Providers =====
147
+ HEADERS = {
148
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
149
+ "Accept": "application/json"
150
+ }
151
+
152
+
153
+ async def fetch_coingecko_simple_price() -> Dict[str, Any]:
154
+ """Fetch real price data from CoinGecko API"""
155
+ url = "https://api.coingecko.com/api/v3/simple/price"
156
+ params = {
157
+ "ids": "bitcoin,ethereum,binancecoin",
158
+ "vs_currencies": "usd",
159
+ "include_market_cap": "true",
160
+ "include_24hr_vol": "true",
161
+ "include_24hr_change": "true"
162
+ }
163
+
164
+ async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
165
+ response = await client.get(url, params=params)
166
+ if response.status_code != 200:
167
+ raise HTTPException(status_code=503, detail=f"CoinGecko API error: HTTP {response.status_code}")
168
+ return response.json()
169
+
170
+
171
+ async def fetch_fear_greed_index() -> Dict[str, Any]:
172
+ """Fetch real Fear & Greed Index from Alternative.me"""
173
+ url = "https://api.alternative.me/fng/"
174
+ params = {"limit": "1", "format": "json"}
175
+
176
+ async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
177
+ response = await client.get(url, params=params)
178
+ if response.status_code != 200:
179
+ raise HTTPException(status_code=503, detail=f"Alternative.me API error: HTTP {response.status_code}")
180
+ return response.json()
181
+
182
+
183
+ async def fetch_coingecko_trending() -> Dict[str, Any]:
184
+ """Fetch real trending coins from CoinGecko"""
185
+ url = "https://api.coingecko.com/api/v3/search/trending"
186
+
187
+ async with httpx.AsyncClient(timeout=15.0, headers=HEADERS) as client:
188
+ response = await client.get(url)
189
+ if response.status_code != 200:
190
+ raise HTTPException(status_code=503, detail=f"CoinGecko trending API error: HTTP {response.status_code}")
191
+ return response.json()
192
+
193
+
194
+ # ===== Lifespan Management =====
195
+ @asynccontextmanager
196
+ async def lifespan(app: FastAPI):
197
+ """Application lifespan manager"""
198
+ print("=" * 80)
199
+ print("🚀 Starting Crypto Monitor Admin API")
200
+ print("=" * 80)
201
+ init_database()
202
+
203
+ # Load providers
204
+ config = load_providers_config()
205
+ _provider_state["providers"] = config.get("providers", {})
206
+ print(f"✓ Loaded {len(_provider_state['providers'])} providers from config")
207
+
208
+ # Load APL report
209
+ apl_report = load_apl_report()
210
+ if apl_report:
211
+ print(f"✓ Loaded APL report with validation data")
212
+
213
+ print(f"✓ Server ready on port {PORT}")
214
+ print("=" * 80)
215
+ yield
216
+ print("Shutting down...")
217
+
218
+
219
+ # ===== FastAPI Application =====
220
+ app = FastAPI(
221
+ title="Crypto Monitor Admin API",
222
+ description="Real-time cryptocurrency data API with Admin Dashboard",
223
+ version="5.0.0",
224
+ lifespan=lifespan
225
+ )
226
+
227
+ # CORS Middleware
228
+ app.add_middleware(
229
+ CORSMiddleware,
230
+ allow_origins=["*"],
231
+ allow_credentials=True,
232
+ allow_methods=["*"],
233
+ allow_headers=["*"],
234
+ )
235
+
236
+ # Mount static files
237
+ try:
238
+ static_path = WORKSPACE_ROOT / "static"
239
+ if static_path.exists():
240
+ app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
241
+ print(f"✓ Mounted static files from {static_path}")
242
+ except Exception as e:
243
+ print(f"⚠ Could not mount static files: {e}")
244
+
245
+
246
+ # ===== HTML UI Endpoints =====
247
+ @app.get("/", response_class=HTMLResponse)
248
+ async def serve_admin_dashboard():
249
+ """Serve admin dashboard"""
250
+ html_path = WORKSPACE_ROOT / "index.html"
251
+ if html_path.exists():
252
+ return FileResponse(html_path)
253
+ return HTMLResponse("<h1>Admin Dashboard</h1><p>admin.html not found</p>")
254
+
255
+
256
+ # ===== Health & Status Endpoints =====
257
+ @app.get("/health")
258
+ async def health():
259
+ """Health check endpoint"""
260
+ return {
261
+ "status": "healthy",
262
+ "timestamp": datetime.now().isoformat(),
263
+ "database": str(DB_PATH),
264
+ "use_mock_data": USE_MOCK_DATA,
265
+ "providers_loaded": len(_provider_state["providers"])
266
+ }
267
+
268
+
269
+ @app.get("/api/status")
270
+ async def get_status():
271
+ """System status"""
272
+ config = load_providers_config()
273
+ providers = config.get("providers", {})
274
+
275
+ # Count by validation status
276
+ validated_count = sum(1 for p in providers.values() if p.get("validated"))
277
+
278
+ return {
279
+ "system_health": "healthy",
280
+ "timestamp": datetime.now().isoformat(),
281
+ "total_providers": len(providers),
282
+ "validated_providers": validated_count,
283
+ "database_status": "connected",
284
+ "apl_available": APL_REPORT_PATH.exists(),
285
+ "use_mock_data": USE_MOCK_DATA
286
+ }
287
+
288
+
289
+ @app.get("/api/stats")
290
+ async def get_stats():
291
+ """System statistics"""
292
+ config = load_providers_config()
293
+ providers = config.get("providers", {})
294
+
295
+ # Group by category
296
+ categories = defaultdict(int)
297
+ for p in providers.values():
298
+ cat = p.get("category", "unknown")
299
+ categories[cat] += 1
300
+
301
+ return {
302
+ "total_providers": len(providers),
303
+ "categories": dict(categories),
304
+ "total_categories": len(categories),
305
+ "timestamp": datetime.now().isoformat()
306
+ }
307
+
308
+
309
+ # ===== Market Data Endpoint =====
310
+ @app.get("/api/market")
311
+ async def get_market_data():
312
+ """Market data from CoinGecko - REAL DATA ONLY"""
313
+ try:
314
+ data = await fetch_coingecko_simple_price()
315
+
316
+ cryptocurrencies = []
317
+ coin_mapping = {
318
+ "bitcoin": {"name": "Bitcoin", "symbol": "BTC", "rank": 1, "image": "https://assets.coingecko.com/coins/images/1/small/bitcoin.png"},
319
+ "ethereum": {"name": "Ethereum", "symbol": "ETH", "rank": 2, "image": "https://assets.coingecko.com/coins/images/279/small/ethereum.png"},
320
+ "binancecoin": {"name": "BNB", "symbol": "BNB", "rank": 3, "image": "https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png"}
321
+ }
322
+
323
+ for coin_id, coin_info in coin_mapping.items():
324
+ if coin_id in data:
325
+ coin_data = data[coin_id]
326
+ crypto_entry = {
327
+ "rank": coin_info["rank"],
328
+ "name": coin_info["name"],
329
+ "symbol": coin_info["symbol"],
330
+ "price": coin_data.get("usd", 0),
331
+ "change_24h": coin_data.get("usd_24h_change", 0),
332
+ "market_cap": coin_data.get("usd_market_cap", 0),
333
+ "volume_24h": coin_data.get("usd_24h_vol", 0),
334
+ "image": coin_info["image"]
335
+ }
336
+ cryptocurrencies.append(crypto_entry)
337
+
338
+ # Save to database
339
+ save_price_to_db({
340
+ "symbol": coin_info["symbol"],
341
+ "name": coin_info["name"],
342
+ "price_usd": crypto_entry["price"],
343
+ "volume_24h": crypto_entry["volume_24h"],
344
+ "market_cap": crypto_entry["market_cap"],
345
+ "percent_change_24h": crypto_entry["change_24h"],
346
+ "rank": coin_info["rank"]
347
+ })
348
+
349
+ # Calculate dominance
350
+ total_market_cap = sum(c["market_cap"] for c in cryptocurrencies)
351
+ btc_dominance = 0
352
+ if total_market_cap > 0:
353
+ btc_entry = next((c for c in cryptocurrencies if c["symbol"] == "BTC"), None)
354
+ if btc_entry:
355
+ btc_dominance = (btc_entry["market_cap"] / total_market_cap) * 100
356
+
357
+ return {
358
+ "cryptocurrencies": cryptocurrencies,
359
+ "total_market_cap": total_market_cap,
360
+ "btc_dominance": btc_dominance,
361
+ "timestamp": datetime.now().isoformat(),
362
+ "source": "CoinGecko API (Real Data)"
363
+ }
364
+
365
+ except Exception as e:
366
+ raise HTTPException(status_code=503, detail=f"Failed to fetch market data: {str(e)}")
367
+
368
+
369
+ @app.get("/api/market/history")
370
+ async def get_market_history(symbol: str = "BTC", limit: int = 10):
371
+ """Get price history from database - REAL DATA ONLY"""
372
+ history = get_price_history_from_db(symbol.upper(), limit)
373
+
374
+ if not history:
375
+ return {
376
+ "symbol": symbol,
377
+ "history": [],
378
+ "count": 0,
379
+ "message": "No history available"
380
+ }
381
+
382
+ return {
383
+ "symbol": symbol,
384
+ "history": history,
385
+ "count": len(history),
386
+ "source": "SQLite Database (Real Data)"
387
+ }
388
+
389
+
390
+ @app.get("/api/sentiment")
391
+ async def get_sentiment():
392
+ """Sentiment data from Alternative.me - REAL DATA ONLY"""
393
+ try:
394
+ data = await fetch_fear_greed_index()
395
+
396
+ if "data" in data and len(data["data"]) > 0:
397
+ fng_data = data["data"][0]
398
+ return {
399
+ "fear_greed_index": int(fng_data["value"]),
400
+ "fear_greed_label": fng_data["value_classification"],
401
+ "timestamp": datetime.now().isoformat(),
402
+ "source": "Alternative.me API (Real Data)"
403
+ }
404
+
405
+ raise HTTPException(status_code=503, detail="Invalid response from Alternative.me")
406
+
407
+ except Exception as e:
408
+ raise HTTPException(status_code=503, detail=f"Failed to fetch sentiment: {str(e)}")
409
+
410
+
411
+ @app.get("/api/trending")
412
+ async def get_trending():
413
+ """Trending coins from CoinGecko - REAL DATA ONLY"""
414
+ try:
415
+ data = await fetch_coingecko_trending()
416
+
417
+ trending_coins = []
418
+ if "coins" in data:
419
+ for item in data["coins"][:10]:
420
+ coin = item.get("item", {})
421
+ trending_coins.append({
422
+ "id": coin.get("id"),
423
+ "name": coin.get("name"),
424
+ "symbol": coin.get("symbol"),
425
+ "market_cap_rank": coin.get("market_cap_rank"),
426
+ "thumb": coin.get("thumb"),
427
+ "score": coin.get("score", 0)
428
+ })
429
+
430
+ return {
431
+ "trending": trending_coins,
432
+ "count": len(trending_coins),
433
+ "timestamp": datetime.now().isoformat(),
434
+ "source": "CoinGecko API (Real Data)"
435
+ }
436
+
437
+ except Exception as e:
438
+ raise HTTPException(status_code=503, detail=f"Failed to fetch trending: {str(e)}")
439
+
440
+
441
+ # ===== Providers Management Endpoints =====
442
+ @app.get("/api/providers")
443
+ async def get_providers():
444
+ """Get all providers - REAL DATA from config"""
445
+ config = load_providers_config()
446
+ providers = config.get("providers", {})
447
+
448
+ result = []
449
+ for provider_id, provider_data in providers.items():
450
+ result.append({
451
+ "provider_id": provider_id,
452
+ "name": provider_data.get("name", provider_id),
453
+ "category": provider_data.get("category", "unknown"),
454
+ "type": provider_data.get("type", "unknown"),
455
+ "status": "validated" if provider_data.get("validated") else "unvalidated",
456
+ "validated_at": provider_data.get("validated_at"),
457
+ "response_time_ms": provider_data.get("response_time_ms"),
458
+ "added_by": provider_data.get("added_by", "manual")
459
+ })
460
+
461
+ return {
462
+ "providers": result,
463
+ "total": len(result),
464
+ "source": "providers_config_extended.json (Real Data)"
465
+ }
466
+
467
+
468
+ @app.get("/api/providers/{provider_id}")
469
+ async def get_provider_detail(provider_id: str):
470
+ """Get specific provider details"""
471
+ config = load_providers_config()
472
+ providers = config.get("providers", {})
473
+
474
+ if provider_id not in providers:
475
+ raise HTTPException(status_code=404, detail=f"Provider {provider_id} not found")
476
+
477
+ return {
478
+ "provider_id": provider_id,
479
+ **providers[provider_id]
480
+ }
481
+
482
+
483
+ @app.get("/api/providers/category/{category}")
484
+ async def get_providers_by_category(category: str):
485
+ """Get providers by category"""
486
+ config = load_providers_config()
487
+ providers = config.get("providers", {})
488
+
489
+ filtered = {
490
+ pid: data for pid, data in providers.items()
491
+ if data.get("category") == category
492
+ }
493
+
494
+ return {
495
+ "category": category,
496
+ "providers": filtered,
497
+ "count": len(filtered)
498
+ }
499
+
500
+
501
+ # ===== Pools Endpoints (Placeholder - to be implemented) =====
502
+ @app.get("/api/pools")
503
+ async def get_pools():
504
+ """Get provider pools"""
505
+ return {
506
+ "pools": [],
507
+ "message": "Pools feature not yet implemented in this version"
508
+ }
509
+
510
+
511
+ # ===== Logs Endpoints =====
512
+ @app.get("/api/logs/recent")
513
+ async def get_recent_logs():
514
+ """Get recent logs"""
515
+ return {
516
+ "logs": _provider_state.get("logs", [])[-50:],
517
+ "count": min(50, len(_provider_state.get("logs", [])))
518
+ }
519
+
520
+
521
+ @app.get("/api/logs/errors")
522
+ async def get_error_logs():
523
+ """Get error logs"""
524
+ all_logs = _provider_state.get("logs", [])
525
+ errors = [log for log in all_logs if log.get("level") == "ERROR"]
526
+ return {
527
+ "errors": errors[-50:],
528
+ "count": len(errors)
529
+ }
530
+
531
+
532
+ # ===== Diagnostics Endpoints =====
533
+ @app.post("/api/diagnostics/run")
534
+ async def run_diagnostics(auto_fix: bool = False):
535
+ """Run system diagnostics"""
536
+ issues = []
537
+ fixes_applied = []
538
+
539
+ # Check database
540
+ if not DB_PATH.exists():
541
+ issues.append({"type": "database", "message": "Database file not found"})
542
+ if auto_fix:
543
+ init_database()
544
+ fixes_applied.append("Initialized database")
545
+
546
+ # Check providers config
547
+ if not PROVIDERS_CONFIG_PATH.exists():
548
+ issues.append({"type": "config", "message": "Providers config not found"})
549
+
550
+ # Check APL report
551
+ if not APL_REPORT_PATH.exists():
552
+ issues.append({"type": "apl", "message": "APL report not found"})
553
+
554
+ return {
555
+ "status": "completed",
556
+ "issues_found": len(issues),
557
+ "issues": issues,
558
+ "fixes_applied": fixes_applied if auto_fix else [],
559
+ "timestamp": datetime.now().isoformat()
560
+ }
561
+
562
+
563
+ @app.get("/api/diagnostics/last")
564
+ async def get_last_diagnostics():
565
+ """Get last diagnostics results"""
566
+ # Would load from file in real implementation
567
+ return {
568
+ "status": "no_previous_run",
569
+ "message": "No previous diagnostics run found"
570
+ }
571
+
572
+
573
+ # ===== APL (Auto Provider Loader) Endpoints =====
574
+ @app.post("/api/apl/run")
575
+ async def run_apl_scan():
576
+ """Run APL provider scan"""
577
+ try:
578
+ # Run APL script
579
+ result = subprocess.run(
580
+ ["python3", str(WORKSPACE_ROOT / "auto_provider_loader.py")],
581
+ capture_output=True,
582
+ text=True,
583
+ timeout=300,
584
+ cwd=str(WORKSPACE_ROOT)
585
+ )
586
+
587
+ # Reload providers after APL run
588
+ config = load_providers_config()
589
+ _provider_state["providers"] = config.get("providers", {})
590
+
591
+ return {
592
+ "status": "completed",
593
+ "stdout": result.stdout[-1000:], # Last 1000 chars
594
+ "returncode": result.returncode,
595
+ "providers_count": len(_provider_state["providers"]),
596
+ "timestamp": datetime.now().isoformat()
597
+ }
598
+
599
+ except subprocess.TimeoutExpired:
600
+ return {
601
+ "status": "timeout",
602
+ "message": "APL scan timed out after 5 minutes"
603
+ }
604
+ except Exception as e:
605
+ raise HTTPException(status_code=500, detail=f"APL scan failed: {str(e)}")
606
+
607
+
608
+ @app.get("/api/apl/report")
609
+ async def get_apl_report():
610
+ """Get APL validation report"""
611
+ report = load_apl_report()
612
+
613
+ if not report:
614
+ return {
615
+ "status": "not_available",
616
+ "message": "APL report not found. Run APL scan first."
617
+ }
618
+
619
+ return report
620
+
621
+
622
+ @app.get("/api/apl/summary")
623
+ async def get_apl_summary():
624
+ """Get APL summary statistics"""
625
+ report = load_apl_report()
626
+
627
+ if not report or "stats" not in report:
628
+ return {
629
+ "status": "not_available",
630
+ "message": "APL report not found"
631
+ }
632
+
633
+ stats = report.get("stats", {})
634
+ return {
635
+ "http_candidates": stats.get("total_http_candidates", 0),
636
+ "http_valid": stats.get("http_valid", 0),
637
+ "http_invalid": stats.get("http_invalid", 0),
638
+ "http_conditional": stats.get("http_conditional", 0),
639
+ "hf_candidates": stats.get("total_hf_candidates", 0),
640
+ "hf_valid": stats.get("hf_valid", 0),
641
+ "hf_invalid": stats.get("hf_invalid", 0),
642
+ "hf_conditional": stats.get("hf_conditional", 0),
643
+ "total_active": stats.get("total_active_providers", 0),
644
+ "timestamp": stats.get("timestamp", "")
645
+ }
646
+
647
+
648
+ # ===== HF Models Endpoints =====
649
+ @app.get("/api/hf/models")
650
+ async def get_hf_models():
651
+ """Get HuggingFace models from APL report"""
652
+ report = load_apl_report()
653
+
654
+ if not report:
655
+ return {"models": [], "count": 0}
656
+
657
+ hf_models = report.get("hf_models", {}).get("results", [])
658
+
659
+ return {
660
+ "models": hf_models,
661
+ "count": len(hf_models),
662
+ "source": "APL Validation Report (Real Data)"
663
+ }
664
+
665
+
666
+ @app.get("/api/hf/health")
667
+ async def get_hf_health():
668
+ """Get HF services health"""
669
+ try:
670
+ from backend.services.hf_registry import REGISTRY
671
+ health = REGISTRY.health()
672
+ return health
673
+ except Exception as e:
674
+ return {
675
+ "ok": False,
676
+ "error": f"HF registry not available: {str(e)}"
677
+ }
678
+
679
+
680
+ # ===== DeFi Endpoint - NOT IMPLEMENTED =====
681
+ @app.get("/api/defi")
682
+ async def get_defi():
683
+ """DeFi endpoint - Not implemented"""
684
+ raise HTTPException(status_code=503, detail="DeFi endpoint not implemented. Real data only - no fakes.")
685
+
686
+
687
+ # ===== HuggingFace ML Sentiment - NOT IMPLEMENTED =====
688
+ @app.post("/api/hf/run-sentiment")
689
+ async def run_sentiment(data: Dict[str, Any]):
690
+ """ML sentiment analysis - Not implemented"""
691
+ raise HTTPException(status_code=501, detail="ML sentiment not implemented. Real data only - no fakes.")
692
+
693
+
694
+ # ===== Main Entry Point =====
695
+ if __name__ == "__main__":
696
+ import uvicorn
697
+ print(f"Starting Crypto Monitor Admin Server on port {PORT}")
698
+ uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")