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

Upload 322 files

Browse files
Files changed (2) hide show
  1. api_server_extended.py +734 -698
  2. hf_unified_server.py +0 -190
api_server_extended.py CHANGED
@@ -1,698 +1,734 @@
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 / "admin.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")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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")
hf_unified_server.py CHANGED
@@ -35,8 +35,6 @@ import json
35
  from pathlib import Path
36
  import httpx
37
 
38
- from database import CryptoDatabase
39
-
40
  from ai_models import (
41
  analyze_chart_points,
42
  analyze_crypto_sentiment,
@@ -86,27 +84,6 @@ provider_collector = ProviderStatusCollector()
86
  WORKSPACE_ROOT = Path(__file__).parent
87
  PROVIDERS_CONFIG_PATH = settings.providers_config_path
88
  FALLBACK_RESOURCE_PATH = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
89
- LOG_DIR = WORKSPACE_ROOT / "logs"
90
- APL_REPORT_PATH = WORKSPACE_ROOT / "PROVIDER_AUTO_DISCOVERY_REPORT.json"
91
-
92
- # Ensure log directory exists
93
- LOG_DIR.mkdir(parents=True, exist_ok=True)
94
-
95
- # Initialize SQLite database for real market data persistence
96
- db = CryptoDatabase()
97
-
98
- def tail_log_file(path: Path, max_lines: int = 200) -> List[str]:
99
- """Return the last max_lines from a log file, if it exists."""
100
- if not path.exists():
101
- return []
102
- try:
103
- with path.open("r", encoding="utf-8", errors="ignore") as f:
104
- lines = f.readlines()
105
- return lines[-max_lines:]
106
- except Exception as e:
107
- logger.error(f"Error reading log file {path}: {e}")
108
- return []
109
-
110
 
111
  def load_providers_config():
112
  """Load providers from providers_config_extended.json"""
@@ -666,24 +643,6 @@ async def get_market():
666
  if not prices:
667
  raise HTTPException(status_code=503, detail="Unable to fetch market data")
668
 
669
- # Persist real market data into SQLite database (no mocks)
670
- try:
671
- for item in prices:
672
- payload = {
673
- "symbol": item.get("symbol", "").upper(),
674
- "name": item.get("name"),
675
- "price_usd": item.get("current_price") or item.get("price"),
676
- "volume_24h": item.get("total_volume"),
677
- "market_cap": item.get("market_cap"),
678
- "percent_change_1h": item.get("price_change_1h") or item.get("price_change_percentage_1h_in_currency"),
679
- "percent_change_24h": item.get("price_change_percentage_24h"),
680
- "percent_change_7d": item.get("price_change_percentage_7d_in_currency"),
681
- "rank": item.get("market_cap_rank"),
682
- }
683
- db.save_price(payload)
684
- except Exception as db_err:
685
- logger.warning(f"Failed to save market data to DB: {db_err}")
686
-
687
  return {
688
  "total_market_cap": overview.get("total_market_cap", 0),
689
  "btc_dominance": overview.get("btc_dominance", 0),
@@ -2454,152 +2413,3 @@ async def websocket_endpoint(websocket: WebSocket):
2454
  ws_manager.disconnect(websocket)
2455
  except:
2456
  pass
2457
-
2458
- @app.get("/api/market/history")
2459
- async def get_market_history(symbol: str = "BTC", limit: int = 10):
2460
- """
2461
- Get historical prices from the real SQLite database.
2462
-
2463
- This uses only stored real market data inserted by /api/market
2464
- and never generates synthetic or mock values.
2465
- """
2466
- symbol = symbol.upper()
2467
- try:
2468
- history = db.get_price_history(symbol, limit=limit)
2469
- except Exception as e:
2470
- logger.error(f"Error reading history for {symbol}: {e}")
2471
- raise HTTPException(status_code=500, detail="Error reading market history")
2472
-
2473
- if not history:
2474
- return {
2475
- "symbol": symbol,
2476
- "history": [],
2477
- "count": 0,
2478
- "message": "No history available",
2479
- }
2480
-
2481
- return {
2482
- "symbol": symbol,
2483
- "history": history,
2484
- "count": len(history),
2485
- "source": "SQLite database (real data)",
2486
- }
2487
-
2488
-
2489
- @app.get("/api/status")
2490
- async def get_status():
2491
- """
2492
- System status endpoint used by the admin UI.
2493
-
2494
- This reports real-time information about providers and database,
2495
- without fabricating any market data.
2496
- """
2497
- providers_cfg = load_providers_config()
2498
- providers = providers_cfg or {}
2499
- validated_count = sum(1 for p in providers.values() if p.get("validated"))
2500
-
2501
- db_path = Path(db.db_path)
2502
- db_status = "connected" if db_path.exists() else "initializing"
2503
-
2504
- return {
2505
- "system_health": "healthy",
2506
- "timestamp": datetime.now().isoformat(),
2507
- "total_providers": len(providers),
2508
- "validated_providers": validated_count,
2509
- "database_status": db_status,
2510
- "apl_available": APL_REPORT_PATH.exists(),
2511
- "use_mock_data": False,
2512
- }
2513
-
2514
-
2515
- @app.get("/api/logs/recent")
2516
- async def get_recent_logs():
2517
- """
2518
- Return recent log lines for the admin UI.
2519
-
2520
- We read from the main server log file if available.
2521
- This does not fabricate content; if there are no logs,
2522
- an empty list is returned.
2523
- """
2524
- log_file = LOG_DIR / "server.log"
2525
- lines = tail_log_file(log_file, max_lines=200)
2526
- # Wrap plain text lines as structured entries
2527
- logs = [{"line": line.rstrip("\n")} for line in lines]
2528
- return {"logs": logs, "count": len(logs)}
2529
-
2530
-
2531
- @app.get("/api/logs/errors")
2532
- async def get_error_logs():
2533
- """
2534
- Return recent error log lines from the same log file.
2535
-
2536
- This is a best-effort filter based on typical ERROR prefixes.
2537
- """
2538
- log_file = LOG_DIR / "server.log"
2539
- lines = tail_log_file(log_file, max_lines=400)
2540
- error_lines = [line for line in lines if "ERROR" in line or "WARNING" in line]
2541
- logs = [{"line": line.rstrip("\n")} for line in error_lines[-200:]]
2542
- return {"errors": logs, "count": len(logs)}
2543
-
2544
-
2545
- def _load_apl_report() -> Optional[Dict[str, Any]]:
2546
- """Load the APL (Auto Provider Loader) validation report if available."""
2547
- if not APL_REPORT_PATH.exists():
2548
- return None
2549
- try:
2550
- with APL_REPORT_PATH.open("r", encoding="utf-8") as f:
2551
- return json.load(f)
2552
- except Exception as e:
2553
- logger.error(f"Error reading APL report: {e}")
2554
- return None
2555
-
2556
-
2557
- @app.get("/api/apl/summary")
2558
- async def get_apl_summary():
2559
- """
2560
- Summary of the Auto Provider Loader (APL) report.
2561
-
2562
- If the report is missing, we return a clear not_available status
2563
- instead of fabricating metrics.
2564
- """
2565
- report = _load_apl_report()
2566
- if not report or "stats" not in report:
2567
- return {
2568
- "status": "not_available",
2569
- "message": "APL report not found",
2570
- }
2571
-
2572
- stats = report.get("stats", {})
2573
- return {
2574
- "status": "ok",
2575
- "http_candidates": stats.get("total_http_candidates", 0),
2576
- "http_valid": stats.get("http_valid", 0),
2577
- "http_invalid": stats.get("http_invalid", 0),
2578
- "http_conditional": stats.get("http_conditional", 0),
2579
- "hf_candidates": stats.get("total_hf_candidates", 0),
2580
- "hf_valid": stats.get("hf_valid", 0),
2581
- "hf_invalid": stats.get("hf_invalid", 0),
2582
- "hf_conditional": stats.get("hf_conditional", 0),
2583
- "timestamp": datetime.now().isoformat(),
2584
- }
2585
-
2586
-
2587
- @app.get("/api/hf/models")
2588
- async def get_hf_models_from_apl():
2589
- """
2590
- Return the list of Hugging Face models discovered by the APL report.
2591
-
2592
- This is used by the admin UI. The data comes from the real
2593
- PROVIDER_AUTO_DISCOVERY_REPORT.json file if present.
2594
- """
2595
- report = _load_apl_report()
2596
- if not report:
2597
- return {"models": [], "count": 0, "source": "none"}
2598
-
2599
- hf_models = report.get("hf_models", {}).get("results", [])
2600
- return {
2601
- "models": hf_models,
2602
- "count": len(hf_models),
2603
- "source": "APL report",
2604
- }
2605
-
 
35
  from pathlib import Path
36
  import httpx
37
 
 
 
38
  from ai_models import (
39
  analyze_chart_points,
40
  analyze_crypto_sentiment,
 
84
  WORKSPACE_ROOT = Path(__file__).parent
85
  PROVIDERS_CONFIG_PATH = settings.providers_config_path
86
  FALLBACK_RESOURCE_PATH = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
  def load_providers_config():
89
  """Load providers from providers_config_extended.json"""
 
643
  if not prices:
644
  raise HTTPException(status_code=503, detail="Unable to fetch market data")
645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  return {
647
  "total_market_cap": overview.get("total_market_cap", 0),
648
  "btc_dominance": overview.get("btc_dominance", 0),
 
2413
  ws_manager.disconnect(websocket)
2414
  except:
2415
  pass