Upload 322 files
Browse files- Dockerfile +1 -1
- api_server_extended.py +1 -1
- hf_unified_server.py +190 -0
Dockerfile
CHANGED
|
@@ -21,4 +21,4 @@ ENV PYTHONUNBUFFERED=1
|
|
| 21 |
EXPOSE 7860
|
| 22 |
|
| 23 |
# Launch command
|
| 24 |
-
CMD ["uvicorn", "
|
|
|
|
| 21 |
EXPOSE 7860
|
| 22 |
|
| 23 |
# Launch command
|
| 24 |
+
CMD ["uvicorn", "hf_unified_server:app", "--host", "0.0.0.0", "--port", "7860"]
|
api_server_extended.py
CHANGED
|
@@ -247,7 +247,7 @@ except Exception as e:
|
|
| 247 |
@app.get("/", response_class=HTMLResponse)
|
| 248 |
async def serve_admin_dashboard():
|
| 249 |
"""Serve admin dashboard"""
|
| 250 |
-
html_path = WORKSPACE_ROOT / "
|
| 251 |
if html_path.exists():
|
| 252 |
return FileResponse(html_path)
|
| 253 |
return HTMLResponse("<h1>Admin Dashboard</h1><p>admin.html not found</p>")
|
|
|
|
| 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>")
|
hf_unified_server.py
CHANGED
|
@@ -35,6 +35,8 @@ import json
|
|
| 35 |
from pathlib import Path
|
| 36 |
import httpx
|
| 37 |
|
|
|
|
|
|
|
| 38 |
from ai_models import (
|
| 39 |
analyze_chart_points,
|
| 40 |
analyze_crypto_sentiment,
|
|
@@ -84,6 +86,27 @@ provider_collector = ProviderStatusCollector()
|
|
| 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,6 +666,24 @@ async def get_market():
|
|
| 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,3 +2454,152 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 2413 |
ws_manager.disconnect(websocket)
|
| 2414 |
except:
|
| 2415 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
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 |
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 |
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 |
+
|