Cryptocurrency data source functionality (#124)
Browse files* Refactor: Disable looping animations and update coverage reports
This commit disables all looping animations globally to prevent visual glitches. It also updates the coverage reports to reflect the current state of code coverage.
Co-authored-by: bxsfy712 <[email protected]>
* feat: Add model selection and health checks to models page
Co-authored-by: bxsfy712 <[email protected]>
* feat: Add support endpoints for fualt.txt and realendpoint.txt
Co-authored-by: bxsfy712 <[email protected]>
* feat: Add DeFi, Fear & Greed, and OHLCV endpoints
This commit introduces new API endpoints for DeFi data from DefiLlama, the Fear & Greed Index, and various OHLCV query styles. It also updates the frontend configuration helper to include these new endpoints and improve code snippets.
Co-authored-by: bxsfy712 <[email protected]>
* Refactor: Move toast defaults to config
Co-authored-by: bxsfy712 <[email protected]>
* Refactor: Improve sentiment display and styling
Enhance the visual presentation of sentiment analysis results with updated CSS and dynamic styling.
Co-authored-by: bxsfy712 <[email protected]>
* feat: Add API key status to dashboard and resource summary
Co-authored-by: bxsfy712 <[email protected]>
---------
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: bxsfy712 <[email protected]>
- coverage.xml +0 -0
- hf_unified_server.py +363 -12
- static/pages/dashboard/dashboard.js +11 -7
- static/pages/models/models.css +117 -18
- static/pages/models/models.js +200 -21
- static/shared/components/config-helper-modal.js +198 -66
- static/shared/css/global.css +39 -0
- static/shared/js/components/toast.js +16 -2
- static/shared/js/core/config.js +10 -0
- static/shared/js/core/support-client.js +26 -0
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -7,7 +7,7 @@ Multi-page architecture with HTTP polling and WebSocket support.
|
|
| 7 |
|
| 8 |
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
-
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse, RedirectResponse
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from contextlib import asynccontextmanager
|
| 13 |
from pathlib import Path
|
|
@@ -18,6 +18,7 @@ import json
|
|
| 18 |
import asyncio
|
| 19 |
import sys
|
| 20 |
import os
|
|
|
|
| 21 |
from typing import List, Dict, Any, Optional, Tuple
|
| 22 |
from pydantic import BaseModel
|
| 23 |
from dotenv import load_dotenv
|
|
@@ -61,6 +62,9 @@ logger = logging.getLogger(__name__)
|
|
| 61 |
WORKSPACE_ROOT = Path(__file__).resolve().parent
|
| 62 |
RESOURCES_FILE = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
|
| 63 |
OHLCV_VERIFICATION_FILE = WORKSPACE_ROOT / "ohlcv_verification_results_20251127_003016.json"
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def _load_json_file(path: Path) -> Optional[Dict[str, Any]]:
|
|
@@ -74,6 +78,89 @@ def _load_json_file(path: Path) -> Optional[Dict[str, Any]]:
|
|
| 74 |
return None
|
| 75 |
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
_RESOURCES_CACHE: Optional[Dict[str, Any]] = _load_json_file(RESOURCES_FILE)
|
| 78 |
_OHLCV_VERIFICATION_CACHE: Optional[Dict[str, Any]] = _load_json_file(OHLCV_VERIFICATION_FILE)
|
| 79 |
|
|
@@ -463,6 +550,84 @@ async def get_all_endpoints():
|
|
| 463 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 464 |
}
|
| 465 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
# ============================================================================
|
| 467 |
# STATIC FILES
|
| 468 |
# ============================================================================
|
|
@@ -663,6 +828,7 @@ async def api_resources_summary() -> Dict[str, Any]:
|
|
| 663 |
"""Resources summary endpoint for dashboard (compatible with frontend)."""
|
| 664 |
try:
|
| 665 |
summary, categories = _summarize_resources()
|
|
|
|
| 666 |
|
| 667 |
# Format for frontend compatibility
|
| 668 |
return {
|
|
@@ -672,6 +838,10 @@ async def api_resources_summary() -> Dict[str, Any]:
|
|
| 672 |
"free_resources": summary.get("free", 0),
|
| 673 |
"premium_resources": summary.get("premium", 0),
|
| 674 |
"models_available": summary.get("models_available", 0),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
"local_routes_count": summary.get("local_routes_count", 0),
|
| 676 |
"categories": {
|
| 677 |
cat["name"].lower().replace(" ", "_"): {
|
|
@@ -695,6 +865,9 @@ async def api_resources_summary() -> Dict[str, Any]:
|
|
| 695 |
"free_resources": 180,
|
| 696 |
"premium_resources": 68,
|
| 697 |
"models_available": 8,
|
|
|
|
|
|
|
|
|
|
| 698 |
"local_routes_count": 24,
|
| 699 |
"categories": {
|
| 700 |
"market_data": {"count": 15, "type": "external"},
|
|
@@ -913,6 +1086,46 @@ async def api_sentiment_global(timeframe: str = "1D"):
|
|
| 913 |
}
|
| 914 |
|
| 915 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
@app.get("/api/sentiment/asset/{symbol}")
|
| 917 |
async def api_sentiment_asset(symbol: str):
|
| 918 |
"""Get sentiment analysis for a specific asset"""
|
|
@@ -1161,6 +1374,66 @@ async def api_news_latest(limit: int = 50) -> Dict[str, Any]:
|
|
| 1161 |
"error": str(e)
|
| 1162 |
}
|
| 1163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1164 |
@app.get("/api/market")
|
| 1165 |
async def api_market(limit: Optional[int] = None):
|
| 1166 |
"""Market overview data - REAL DATA from CoinGecko"""
|
|
@@ -1345,6 +1618,11 @@ async def api_sentiment_analyze(payload: Dict[str, Any]):
|
|
| 1345 |
try:
|
| 1346 |
text = payload.get("text", "")
|
| 1347 |
mode = payload.get("mode", "crypto")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1348 |
|
| 1349 |
if not text:
|
| 1350 |
return {
|
|
@@ -1356,15 +1634,41 @@ async def api_sentiment_analyze(payload: Dict[str, Any]):
|
|
| 1356 |
# Use AI service for sentiment analysis
|
| 1357 |
try:
|
| 1358 |
from backend.services.ai_service_unified import ai_service
|
| 1359 |
-
|
| 1360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1361 |
return {
|
| 1362 |
"success": True,
|
| 1363 |
-
"sentiment": result.get("sentiment", "neutral"),
|
| 1364 |
-
"score": result.get("score", 0.5),
|
| 1365 |
-
"confidence": result.get("confidence", 0.5),
|
| 1366 |
"model": result.get("model", "unified"),
|
| 1367 |
-
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 1368 |
}
|
| 1369 |
except Exception as e:
|
| 1370 |
logger.warning(f"AI sentiment analysis failed: {e}, using fallback")
|
|
@@ -1413,6 +1717,53 @@ async def api_sentiment_analyze(payload: Dict[str, Any]):
|
|
| 1413 |
# OHLCV DATA ENDPOINTS
|
| 1414 |
# ============================================================================
|
| 1415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1416 |
@app.get("/api/ohlcv/{symbol}")
|
| 1417 |
async def api_ohlcv_symbol(symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 1418 |
"""Get OHLCV data for a symbol - fallback endpoint"""
|
|
@@ -1480,11 +1831,11 @@ async def api_ohlcv_multi(symbols: str, timeframe: str = "1h", limit: int = 100)
|
|
| 1480 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 1481 |
}
|
| 1482 |
|
| 1483 |
-
#
|
| 1484 |
-
|
| 1485 |
-
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
|
| 1489 |
# API Root endpoint - Keep for backwards compatibility
|
| 1490 |
@app.get("/api")
|
|
|
|
| 7 |
|
| 8 |
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
| 10 |
+
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse, RedirectResponse, PlainTextResponse
|
| 11 |
from fastapi.staticfiles import StaticFiles
|
| 12 |
from contextlib import asynccontextmanager
|
| 13 |
from pathlib import Path
|
|
|
|
| 18 |
import asyncio
|
| 19 |
import sys
|
| 20 |
import os
|
| 21 |
+
import re
|
| 22 |
from typing import List, Dict, Any, Optional, Tuple
|
| 23 |
from pydantic import BaseModel
|
| 24 |
from dotenv import load_dotenv
|
|
|
|
| 62 |
WORKSPACE_ROOT = Path(__file__).resolve().parent
|
| 63 |
RESOURCES_FILE = WORKSPACE_ROOT / "crypto_resources_unified_2025-11-11.json"
|
| 64 |
OHLCV_VERIFICATION_FILE = WORKSPACE_ROOT / "ohlcv_verification_results_20251127_003016.json"
|
| 65 |
+
FAULT_LOG_FILE = WORKSPACE_ROOT / "fualt.txt"
|
| 66 |
+
REAL_ENDPOINTS_FILE = WORKSPACE_ROOT / "realendpoint.txt"
|
| 67 |
+
API_KEYS_CONFIG_FILE = WORKSPACE_ROOT / "config" / "api_keys.json"
|
| 68 |
|
| 69 |
|
| 70 |
def _load_json_file(path: Path) -> Optional[Dict[str, Any]]:
|
|
|
|
| 78 |
return None
|
| 79 |
|
| 80 |
|
| 81 |
+
def _read_text_file_tail(path: Path, tail: Optional[int] = None) -> Dict[str, Any]:
|
| 82 |
+
"""
|
| 83 |
+
Read a text file safely with optional tail (last N lines).
|
| 84 |
+
Returns structured data for client consumption.
|
| 85 |
+
"""
|
| 86 |
+
if not path.exists():
|
| 87 |
+
return {"exists": False, "path": str(path), "tail": tail, "lines": [], "content": ""}
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
text = path.read_text(encoding="utf-8", errors="replace")
|
| 91 |
+
except Exception as exc: # pragma: no cover
|
| 92 |
+
logger.error("Failed to read %s: %s", path, exc)
|
| 93 |
+
return {"exists": True, "path": str(path), "tail": tail, "lines": [], "content": "", "error": str(exc)}
|
| 94 |
+
|
| 95 |
+
lines = text.splitlines()
|
| 96 |
+
if isinstance(tail, int) and tail > 0:
|
| 97 |
+
lines = lines[-tail:]
|
| 98 |
+
text = "\n".join(lines)
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
"exists": True,
|
| 102 |
+
"path": str(path),
|
| 103 |
+
"tail": tail,
|
| 104 |
+
"line_count": len(lines),
|
| 105 |
+
"content": text,
|
| 106 |
+
"lines": lines,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _count_configured_api_keys() -> Dict[str, Any]:
|
| 111 |
+
"""
|
| 112 |
+
Count API keys configured via environment variables referenced in config/api_keys.json.
|
| 113 |
+
Values in the config are typically placeholders like "${ETHERSCAN_KEY}".
|
| 114 |
+
"""
|
| 115 |
+
try:
|
| 116 |
+
if not API_KEYS_CONFIG_FILE.exists():
|
| 117 |
+
return {
|
| 118 |
+
"config_exists": False,
|
| 119 |
+
"total_key_refs": 0,
|
| 120 |
+
"configured_keys": 0,
|
| 121 |
+
"missing_keys": [],
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
raw = API_KEYS_CONFIG_FILE.read_text(encoding="utf-8", errors="replace")
|
| 125 |
+
cfg = json.loads(raw) if raw.strip() else {}
|
| 126 |
+
|
| 127 |
+
pattern = re.compile(r"\$\{([A-Z0-9_]+)\}")
|
| 128 |
+
referenced: List[str] = []
|
| 129 |
+
|
| 130 |
+
def walk(obj: Any) -> None:
|
| 131 |
+
if isinstance(obj, dict):
|
| 132 |
+
for v in obj.values():
|
| 133 |
+
walk(v)
|
| 134 |
+
elif isinstance(obj, list):
|
| 135 |
+
for v in obj:
|
| 136 |
+
walk(v)
|
| 137 |
+
elif isinstance(obj, str):
|
| 138 |
+
for m in pattern.finditer(obj):
|
| 139 |
+
referenced.append(m.group(1))
|
| 140 |
+
|
| 141 |
+
walk(cfg)
|
| 142 |
+
# Unique + stable order
|
| 143 |
+
refs = sorted(set(referenced))
|
| 144 |
+
configured = [k for k in refs if (os.getenv(k) or "").strip() not in ("", "null", "None")]
|
| 145 |
+
missing = [k for k in refs if k not in configured]
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"config_exists": True,
|
| 149 |
+
"total_key_refs": len(refs),
|
| 150 |
+
"configured_keys": len(configured),
|
| 151 |
+
"missing_keys": missing,
|
| 152 |
+
}
|
| 153 |
+
except Exception as e:
|
| 154 |
+
logger.error(f"Failed to count configured API keys: {e}")
|
| 155 |
+
return {
|
| 156 |
+
"config_exists": bool(API_KEYS_CONFIG_FILE.exists()),
|
| 157 |
+
"total_key_refs": 0,
|
| 158 |
+
"configured_keys": 0,
|
| 159 |
+
"missing_keys": [],
|
| 160 |
+
"error": str(e),
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
|
| 164 |
_RESOURCES_CACHE: Optional[Dict[str, Any]] = _load_json_file(RESOURCES_FILE)
|
| 165 |
_OHLCV_VERIFICATION_CACHE: Optional[Dict[str, Any]] = _load_json_file(OHLCV_VERIFICATION_FILE)
|
| 166 |
|
|
|
|
| 550 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 551 |
}
|
| 552 |
|
| 553 |
+
# ============================================================================
|
| 554 |
+
# SUPPORT FILES (fualt.txt, realendpoint.txt) FOR CLIENTS
|
| 555 |
+
# ============================================================================
|
| 556 |
+
|
| 557 |
+
@app.get("/api/support/fualt")
|
| 558 |
+
async def api_support_fualt(tail: Optional[int] = 500) -> Dict[str, Any]:
|
| 559 |
+
"""
|
| 560 |
+
Expose `fualt.txt` to clients (debug/support).
|
| 561 |
+
Default returns last 500 lines to keep payload small.
|
| 562 |
+
"""
|
| 563 |
+
data = _read_text_file_tail(FAULT_LOG_FILE, tail=tail)
|
| 564 |
+
data["timestamp"] = datetime.utcnow().isoformat() + "Z"
|
| 565 |
+
return data
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
@app.get("/fualt.txt")
|
| 569 |
+
async def download_fualt_txt():
|
| 570 |
+
"""Download the raw `fualt.txt` file if present."""
|
| 571 |
+
if not FAULT_LOG_FILE.exists():
|
| 572 |
+
return PlainTextResponse("fualt.txt not found", status_code=404)
|
| 573 |
+
return FileResponse(FAULT_LOG_FILE)
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def _build_real_endpoints_snapshot() -> List[Dict[str, Any]]:
|
| 577 |
+
"""Build a minimal endpoint snapshot from the current FastAPI routing table."""
|
| 578 |
+
snapshot: List[Dict[str, Any]] = []
|
| 579 |
+
for route in app.routes:
|
| 580 |
+
if not hasattr(route, "path") or not hasattr(route, "methods"):
|
| 581 |
+
continue
|
| 582 |
+
if route.path.startswith("/openapi") or route.path == "/docs":
|
| 583 |
+
continue
|
| 584 |
+
methods = sorted([m for m in (route.methods or []) if m not in {"HEAD", "OPTIONS"}])
|
| 585 |
+
snapshot.append({"path": route.path, "methods": methods, "name": getattr(route, "name", "")})
|
| 586 |
+
|
| 587 |
+
# Sort stable for clients/diffs
|
| 588 |
+
snapshot.sort(key=lambda r: (r["path"], ",".join(r["methods"])))
|
| 589 |
+
return snapshot
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
@app.get("/api/support/realendpoints")
|
| 593 |
+
async def api_support_realendpoints(format: str = "json") -> Any:
|
| 594 |
+
"""
|
| 595 |
+
Provide a "real endpoints" list for clients.
|
| 596 |
+
- format=json (default): structured list
|
| 597 |
+
- format=txt: plain text similar to a `realendpoint.txt` file
|
| 598 |
+
"""
|
| 599 |
+
endpoints_snapshot = _build_real_endpoints_snapshot()
|
| 600 |
+
if format.lower() == "txt":
|
| 601 |
+
lines = []
|
| 602 |
+
for e in endpoints_snapshot:
|
| 603 |
+
methods = ",".join(e["methods"]) if e["methods"] else ""
|
| 604 |
+
lines.append(f"{methods:10} {e['path']}")
|
| 605 |
+
return PlainTextResponse("\n".join(lines) + "\n")
|
| 606 |
+
|
| 607 |
+
return {
|
| 608 |
+
"success": True,
|
| 609 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 610 |
+
"count": len(endpoints_snapshot),
|
| 611 |
+
"endpoints": endpoints_snapshot,
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
@app.get("/realendpoint.txt")
|
| 616 |
+
async def download_realendpoint_txt():
|
| 617 |
+
"""
|
| 618 |
+
Download `realendpoint.txt` if present, otherwise generate it on the fly from
|
| 619 |
+
the live routing table (so it is always "supported").
|
| 620 |
+
"""
|
| 621 |
+
if REAL_ENDPOINTS_FILE.exists():
|
| 622 |
+
return FileResponse(REAL_ENDPOINTS_FILE)
|
| 623 |
+
# Generate on the fly
|
| 624 |
+
endpoints_snapshot = _build_real_endpoints_snapshot()
|
| 625 |
+
lines = []
|
| 626 |
+
for e in endpoints_snapshot:
|
| 627 |
+
methods = ",".join(e["methods"]) if e["methods"] else ""
|
| 628 |
+
lines.append(f"{methods:10} {e['path']}")
|
| 629 |
+
return PlainTextResponse("\n".join(lines) + "\n")
|
| 630 |
+
|
| 631 |
# ============================================================================
|
| 632 |
# STATIC FILES
|
| 633 |
# ============================================================================
|
|
|
|
| 828 |
"""Resources summary endpoint for dashboard (compatible with frontend)."""
|
| 829 |
try:
|
| 830 |
summary, categories = _summarize_resources()
|
| 831 |
+
keys_info = _count_configured_api_keys()
|
| 832 |
|
| 833 |
# Format for frontend compatibility
|
| 834 |
return {
|
|
|
|
| 838 |
"free_resources": summary.get("free", 0),
|
| 839 |
"premium_resources": summary.get("premium", 0),
|
| 840 |
"models_available": summary.get("models_available", 0),
|
| 841 |
+
# API key status (for dashboard)
|
| 842 |
+
"total_api_keys": keys_info.get("total_key_refs", 0),
|
| 843 |
+
"configured_api_keys": keys_info.get("configured_keys", 0),
|
| 844 |
+
"api_keys_config_loaded": keys_info.get("config_exists", False),
|
| 845 |
"local_routes_count": summary.get("local_routes_count", 0),
|
| 846 |
"categories": {
|
| 847 |
cat["name"].lower().replace(" ", "_"): {
|
|
|
|
| 865 |
"free_resources": 180,
|
| 866 |
"premium_resources": 68,
|
| 867 |
"models_available": 8,
|
| 868 |
+
"total_api_keys": 0,
|
| 869 |
+
"configured_api_keys": 0,
|
| 870 |
+
"api_keys_config_loaded": False,
|
| 871 |
"local_routes_count": 24,
|
| 872 |
"categories": {
|
| 873 |
"market_data": {"count": 15, "type": "external"},
|
|
|
|
| 1086 |
}
|
| 1087 |
|
| 1088 |
|
| 1089 |
+
@app.get("/api/fear-greed")
|
| 1090 |
+
async def api_fear_greed(limit: int = 1) -> Dict[str, Any]:
|
| 1091 |
+
"""
|
| 1092 |
+
Convenience endpoint for Fear & Greed Index (Alternative.me).
|
| 1093 |
+
This keeps client integrations simple and is safe to call from the dashboard.
|
| 1094 |
+
"""
|
| 1095 |
+
try:
|
| 1096 |
+
import httpx
|
| 1097 |
+
|
| 1098 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 1099 |
+
response = await client.get("https://api.alternative.me/fng/", params={"limit": str(limit)})
|
| 1100 |
+
response.raise_for_status()
|
| 1101 |
+
payload = response.json()
|
| 1102 |
+
|
| 1103 |
+
data = payload.get("data") or []
|
| 1104 |
+
latest = data[0] if isinstance(data, list) and data else {}
|
| 1105 |
+
|
| 1106 |
+
value = int(latest.get("value", 50))
|
| 1107 |
+
classification = latest.get("value_classification", "Neutral")
|
| 1108 |
+
|
| 1109 |
+
return {
|
| 1110 |
+
"success": True,
|
| 1111 |
+
"value": value,
|
| 1112 |
+
"classification": classification,
|
| 1113 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1114 |
+
"source": "alternative.me",
|
| 1115 |
+
"data": data[: min(len(data), 30)],
|
| 1116 |
+
}
|
| 1117 |
+
except Exception as e:
|
| 1118 |
+
logger.error(f"Failed to fetch Fear & Greed Index: {e}")
|
| 1119 |
+
return {
|
| 1120 |
+
"success": False,
|
| 1121 |
+
"value": 50,
|
| 1122 |
+
"classification": "Neutral",
|
| 1123 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1124 |
+
"source": "unavailable",
|
| 1125 |
+
"error": str(e),
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
|
| 1129 |
@app.get("/api/sentiment/asset/{symbol}")
|
| 1130 |
async def api_sentiment_asset(symbol: str):
|
| 1131 |
"""Get sentiment analysis for a specific asset"""
|
|
|
|
| 1374 |
"error": str(e)
|
| 1375 |
}
|
| 1376 |
|
| 1377 |
+
|
| 1378 |
+
# ============================================================================
|
| 1379 |
+
# DeFi (public, no keys) - DefiLlama
|
| 1380 |
+
# ============================================================================
|
| 1381 |
+
|
| 1382 |
+
@app.get("/api/defi/tvl")
|
| 1383 |
+
async def api_defi_tvl() -> Dict[str, Any]:
|
| 1384 |
+
"""Total Value Locked (TVL) using DefiLlama public API."""
|
| 1385 |
+
try:
|
| 1386 |
+
import httpx
|
| 1387 |
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
| 1388 |
+
resp = await client.get("https://api.llama.fi/tvl")
|
| 1389 |
+
resp.raise_for_status()
|
| 1390 |
+
tvl = resp.json()
|
| 1391 |
+
return {"success": True, "tvl": tvl, "source": "defillama", "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1392 |
+
except Exception as e:
|
| 1393 |
+
logger.error(f"DeFi TVL failed: {e}")
|
| 1394 |
+
return {"success": False, "tvl": None, "source": "unavailable", "error": str(e), "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1395 |
+
|
| 1396 |
+
|
| 1397 |
+
@app.get("/api/defi/protocols")
|
| 1398 |
+
async def api_defi_protocols(limit: int = 20) -> Dict[str, Any]:
|
| 1399 |
+
"""Top DeFi protocols by TVL using DefiLlama public API."""
|
| 1400 |
+
try:
|
| 1401 |
+
import httpx
|
| 1402 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 1403 |
+
resp = await client.get("https://api.llama.fi/protocols")
|
| 1404 |
+
resp.raise_for_status()
|
| 1405 |
+
protocols = resp.json()
|
| 1406 |
+
|
| 1407 |
+
if isinstance(protocols, list):
|
| 1408 |
+
protocols = sorted(protocols, key=lambda p: float(p.get("tvl") or 0), reverse=True)
|
| 1409 |
+
protocols = protocols[: max(1, min(int(limit), 100))]
|
| 1410 |
+
|
| 1411 |
+
return {"success": True, "protocols": protocols, "source": "defillama", "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1412 |
+
except Exception as e:
|
| 1413 |
+
logger.error(f"DeFi protocols failed: {e}")
|
| 1414 |
+
return {"success": False, "protocols": [], "source": "unavailable", "error": str(e), "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1415 |
+
|
| 1416 |
+
|
| 1417 |
+
@app.get("/api/defi/yields")
|
| 1418 |
+
async def api_defi_yields(limit: int = 20) -> Dict[str, Any]:
|
| 1419 |
+
"""Yield pools snapshot using DefiLlama public API."""
|
| 1420 |
+
try:
|
| 1421 |
+
import httpx
|
| 1422 |
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
| 1423 |
+
resp = await client.get("https://yields.llama.fi/pools")
|
| 1424 |
+
resp.raise_for_status()
|
| 1425 |
+
payload = resp.json()
|
| 1426 |
+
|
| 1427 |
+
pools = payload.get("data") if isinstance(payload, dict) else []
|
| 1428 |
+
if isinstance(pools, list):
|
| 1429 |
+
pools = sorted(pools, key=lambda p: float(p.get("tvlUsd") or 0), reverse=True)
|
| 1430 |
+
pools = pools[: max(1, min(int(limit), 100))]
|
| 1431 |
+
|
| 1432 |
+
return {"success": True, "pools": pools, "source": "defillama", "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1433 |
+
except Exception as e:
|
| 1434 |
+
logger.error(f"DeFi yields failed: {e}")
|
| 1435 |
+
return {"success": False, "pools": [], "source": "unavailable", "error": str(e), "timestamp": datetime.utcnow().isoformat() + "Z"}
|
| 1436 |
+
|
| 1437 |
@app.get("/api/market")
|
| 1438 |
async def api_market(limit: Optional[int] = None):
|
| 1439 |
"""Market overview data - REAL DATA from CoinGecko"""
|
|
|
|
| 1618 |
try:
|
| 1619 |
text = payload.get("text", "")
|
| 1620 |
mode = payload.get("mode", "crypto")
|
| 1621 |
+
# Optional: allow explicit HF model selection from the UI
|
| 1622 |
+
# - `model_key`: key from the server/client registry (preferred)
|
| 1623 |
+
# - `model`: backwards-compatible alias used by some pages
|
| 1624 |
+
model_key = payload.get("model_key") or payload.get("model")
|
| 1625 |
+
use_ensemble = bool(payload.get("use_ensemble", True))
|
| 1626 |
|
| 1627 |
if not text:
|
| 1628 |
return {
|
|
|
|
| 1634 |
# Use AI service for sentiment analysis
|
| 1635 |
try:
|
| 1636 |
from backend.services.ai_service_unified import ai_service
|
| 1637 |
+
|
| 1638 |
+
# If the UI requested a specific model_key and HF client is available,
|
| 1639 |
+
# call it directly so the Models page "Test Model" works.
|
| 1640 |
+
if model_key and getattr(ai_service, "hf_client", None) is not None:
|
| 1641 |
+
hf_result = await ai_service.hf_client.analyze_sentiment(
|
| 1642 |
+
text=text,
|
| 1643 |
+
model_key=str(model_key),
|
| 1644 |
+
use_cache=True,
|
| 1645 |
+
)
|
| 1646 |
+
|
| 1647 |
+
# Normalize HF API client response into the UI-friendly shape.
|
| 1648 |
+
if hf_result.get("status") == "success":
|
| 1649 |
+
return {
|
| 1650 |
+
"success": True,
|
| 1651 |
+
"sentiment": hf_result.get("label", "neutral"),
|
| 1652 |
+
"score": hf_result.get("score", hf_result.get("confidence", 0.5)),
|
| 1653 |
+
"confidence": hf_result.get("confidence", hf_result.get("score", 0.5)),
|
| 1654 |
+
"model": hf_result.get("model", "hf_inference_api"),
|
| 1655 |
+
"model_key": hf_result.get("model_key", model_key),
|
| 1656 |
+
"engine": hf_result.get("engine", "hf_inference_api"),
|
| 1657 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1658 |
+
}
|
| 1659 |
+
|
| 1660 |
+
# If the selected model isn't available, fall back to auto mode.
|
| 1661 |
+
# (Still return success=False only if everything fails.)
|
| 1662 |
+
|
| 1663 |
+
result = await ai_service.analyze_sentiment(text, category=mode, use_ensemble=use_ensemble)
|
| 1664 |
+
|
| 1665 |
return {
|
| 1666 |
"success": True,
|
| 1667 |
+
"sentiment": result.get("sentiment", result.get("label", "neutral")),
|
| 1668 |
+
"score": result.get("score", result.get("confidence", 0.5)),
|
| 1669 |
+
"confidence": result.get("confidence", result.get("score", 0.5)),
|
| 1670 |
"model": result.get("model", "unified"),
|
| 1671 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1672 |
}
|
| 1673 |
except Exception as e:
|
| 1674 |
logger.warning(f"AI sentiment analysis failed: {e}, using fallback")
|
|
|
|
| 1717 |
# OHLCV DATA ENDPOINTS
|
| 1718 |
# ============================================================================
|
| 1719 |
|
| 1720 |
+
@app.get("/api/ohlcv")
|
| 1721 |
+
async def api_ohlcv_query(symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 1722 |
+
"""Query-style OHLCV endpoint for frontend compatibility."""
|
| 1723 |
+
return await api_ohlcv_symbol(symbol=symbol, timeframe=timeframe, limit=limit)
|
| 1724 |
+
|
| 1725 |
+
|
| 1726 |
+
@app.get("/api/klines")
|
| 1727 |
+
async def api_klines(symbol: str, interval: str = "1h", limit: int = 100):
|
| 1728 |
+
"""Binance-style klines alias for frontend compatibility."""
|
| 1729 |
+
sym = symbol.upper().strip()
|
| 1730 |
+
m = re.match(r"^([A-Z0-9]+?)(USDT|USD|USDC|BUSD)$", sym)
|
| 1731 |
+
base = m.group(1) if m else sym
|
| 1732 |
+
return await api_ohlcv_symbol(symbol=base, timeframe=interval, limit=limit)
|
| 1733 |
+
|
| 1734 |
+
|
| 1735 |
+
@app.get("/api/historical")
|
| 1736 |
+
async def api_historical(symbol: str, days: int = 30):
|
| 1737 |
+
"""Simple historical alias (daily candles)."""
|
| 1738 |
+
try:
|
| 1739 |
+
from backend.services.binance_client import BinanceClient
|
| 1740 |
+
|
| 1741 |
+
binance = BinanceClient()
|
| 1742 |
+
fetch_days = min(max(int(days), 1), 365)
|
| 1743 |
+
data = await binance.get_ohlcv(symbol.upper(), "1d", fetch_days)
|
| 1744 |
+
return {
|
| 1745 |
+
"success": True,
|
| 1746 |
+
"symbol": symbol.upper(),
|
| 1747 |
+
"days": fetch_days,
|
| 1748 |
+
"data": data,
|
| 1749 |
+
"count": len(data),
|
| 1750 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1751 |
+
"source": "binance",
|
| 1752 |
+
}
|
| 1753 |
+
except Exception as e:
|
| 1754 |
+
logger.warning(f"Historical fetch failed for {symbol}: {e}")
|
| 1755 |
+
return {
|
| 1756 |
+
"success": False,
|
| 1757 |
+
"symbol": symbol.upper(),
|
| 1758 |
+
"days": days,
|
| 1759 |
+
"data": [],
|
| 1760 |
+
"count": 0,
|
| 1761 |
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
| 1762 |
+
"source": "unavailable",
|
| 1763 |
+
"error": str(e),
|
| 1764 |
+
}
|
| 1765 |
+
|
| 1766 |
+
|
| 1767 |
@app.get("/api/ohlcv/{symbol}")
|
| 1768 |
async def api_ohlcv_symbol(symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 1769 |
"""Get OHLCV data for a symbol - fallback endpoint"""
|
|
|
|
| 1831 |
"timestamp": datetime.utcnow().isoformat() + "Z"
|
| 1832 |
}
|
| 1833 |
|
| 1834 |
+
#
|
| 1835 |
+
# NOTE:
|
| 1836 |
+
# `/` is already handled earlier (redirects to dashboard). Keep a single handler
|
| 1837 |
+
# for a stable routing table (avoids duplicate route definitions).
|
| 1838 |
+
#
|
| 1839 |
|
| 1840 |
# API Root endpoint - Keep for backwards compatibility
|
| 1841 |
@app.get("/api")
|
|
@@ -541,22 +541,26 @@ class DashboardPage {
|
|
| 541 |
|
| 542 |
async fetchStats() {
|
| 543 |
try {
|
| 544 |
-
const [res1, res2] = await Promise.allSettled([
|
| 545 |
apiClient.fetch('/api/resources/summary', {}, 15000).then(r => r.ok ? r.json() : null),
|
| 546 |
-
apiClient.fetch('/api/models/status', {}, 10000).then(r => r.ok ? r.json() : null)
|
|
|
|
| 547 |
]);
|
| 548 |
|
| 549 |
const data = res1.value?.summary || res1.value || {};
|
| 550 |
const models = res2.value || {};
|
|
|
|
| 551 |
|
| 552 |
-
//
|
| 553 |
-
const providerCount =
|
| 554 |
-
|
| 555 |
-
(data.
|
|
|
|
| 556 |
|
| 557 |
return {
|
| 558 |
total_resources: data.total_resources || 0,
|
| 559 |
-
|
|
|
|
| 560 |
models_loaded: models.models_loaded || data.models_available || 0,
|
| 561 |
active_providers: providerCount // FIX: Use actual provider count, not total_resources
|
| 562 |
};
|
|
|
|
| 541 |
|
| 542 |
async fetchStats() {
|
| 543 |
try {
|
| 544 |
+
const [res1, res2, res3] = await Promise.allSettled([
|
| 545 |
apiClient.fetch('/api/resources/summary', {}, 15000).then(r => r.ok ? r.json() : null),
|
| 546 |
+
apiClient.fetch('/api/models/status', {}, 10000).then(r => r.ok ? r.json() : null),
|
| 547 |
+
apiClient.fetch('/api/providers', {}, 10000).then(r => r.ok ? r.json() : null)
|
| 548 |
]);
|
| 549 |
|
| 550 |
const data = res1.value?.summary || res1.value || {};
|
| 551 |
const models = res2.value || {};
|
| 552 |
+
const providers = res3.value || {};
|
| 553 |
|
| 554 |
+
// Providers: prefer backend providers endpoint; fallback to categories length if needed
|
| 555 |
+
const providerCount = Number.isFinite(providers?.online) ? providers.online
|
| 556 |
+
: Number.isFinite(providers?.total) ? providers.total
|
| 557 |
+
: Array.isArray(data.by_category) ? data.by_category.length
|
| 558 |
+
: 0;
|
| 559 |
|
| 560 |
return {
|
| 561 |
total_resources: data.total_resources || 0,
|
| 562 |
+
// Show configured keys (real usefulness), fallback to total refs
|
| 563 |
+
api_keys: data.configured_api_keys ?? data.total_api_keys ?? 0,
|
| 564 |
models_loaded: models.models_loaded || data.models_available || 0,
|
| 565 |
active_providers: providerCount // FIX: Use actual provider count, not total_resources
|
| 566 |
};
|
|
@@ -853,12 +853,25 @@
|
|
| 853 |
.test-result {
|
| 854 |
margin-top: var(--space-6);
|
| 855 |
padding: var(--space-6);
|
| 856 |
-
background: rgba(0, 0, 0, 0.
|
| 857 |
-
border: 1px solid rgba(255, 255, 255, 0.
|
| 858 |
border-radius: var(--radius-xl);
|
|
|
|
|
|
|
|
|
|
| 859 |
animation: fadeIn 0.4s ease;
|
| 860 |
}
|
| 861 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
.test-result.hidden {
|
| 863 |
display: none;
|
| 864 |
}
|
|
@@ -868,11 +881,13 @@
|
|
| 868 |
justify-content: space-between;
|
| 869 |
align-items: center;
|
| 870 |
margin-bottom: var(--space-4);
|
|
|
|
|
|
|
| 871 |
}
|
| 872 |
|
| 873 |
.result-header h3 {
|
| 874 |
font-size: var(--font-size-lg);
|
| 875 |
-
font-weight:
|
| 876 |
color: var(--text-strong);
|
| 877 |
margin: 0;
|
| 878 |
}
|
|
@@ -880,40 +895,107 @@
|
|
| 880 |
.result-time {
|
| 881 |
font-size: var(--font-size-xs);
|
| 882 |
color: var(--text-muted);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 883 |
}
|
| 884 |
|
| 885 |
.sentiment-display {
|
| 886 |
-
|
|
|
|
| 887 |
padding: var(--space-6);
|
| 888 |
-
background: linear-gradient(135deg, rgba(
|
|
|
|
| 889 |
border-radius: var(--radius-xl);
|
| 890 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
}
|
| 892 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
.sentiment-emoji {
|
| 894 |
-
|
| 895 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 896 |
}
|
| 897 |
|
| 898 |
.sentiment-label {
|
|
|
|
|
|
|
| 899 |
font-family: 'Space Grotesk', sans-serif;
|
| 900 |
-
font-size:
|
| 901 |
font-weight: 700;
|
| 902 |
text-transform: uppercase;
|
| 903 |
-
|
|
|
|
|
|
|
|
|
|
| 904 |
}
|
| 905 |
|
| 906 |
-
.sentiment-label.bullish
|
| 907 |
-
.sentiment-label.
|
| 908 |
-
.sentiment-label.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
|
| 910 |
.sentiment-confidence {
|
| 911 |
-
|
| 912 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
}
|
| 914 |
|
| 915 |
.result-details {
|
| 916 |
-
background: rgba(0, 0, 0, 0.
|
|
|
|
| 917 |
border-radius: var(--radius-md);
|
| 918 |
padding: var(--space-4);
|
| 919 |
overflow: auto;
|
|
@@ -922,10 +1004,27 @@
|
|
| 922 |
|
| 923 |
.result-json {
|
| 924 |
font-family: 'JetBrains Mono', monospace;
|
| 925 |
-
font-size:
|
| 926 |
-
color:
|
| 927 |
white-space: pre-wrap;
|
| 928 |
margin: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 929 |
}
|
| 930 |
|
| 931 |
/* =========================================================================
|
|
|
|
| 853 |
.test-result {
|
| 854 |
margin-top: var(--space-6);
|
| 855 |
padding: var(--space-6);
|
| 856 |
+
background: linear-gradient(180deg, rgba(2, 6, 23, 0.55), rgba(0, 0, 0, 0.35));
|
| 857 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 858 |
border-radius: var(--radius-xl);
|
| 859 |
+
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
|
| 860 |
+
position: relative;
|
| 861 |
+
overflow: hidden;
|
| 862 |
animation: fadeIn 0.4s ease;
|
| 863 |
}
|
| 864 |
|
| 865 |
+
.test-result::before {
|
| 866 |
+
content: '';
|
| 867 |
+
position: absolute;
|
| 868 |
+
inset: 0;
|
| 869 |
+
pointer-events: none;
|
| 870 |
+
background: radial-gradient(900px 220px at 20% 0%, rgba(139, 92, 246, 0.18), transparent 60%),
|
| 871 |
+
radial-gradient(900px 220px at 80% 0%, rgba(34, 211, 238, 0.14), transparent 60%);
|
| 872 |
+
opacity: 0.9;
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
.test-result.hidden {
|
| 876 |
display: none;
|
| 877 |
}
|
|
|
|
| 881 |
justify-content: space-between;
|
| 882 |
align-items: center;
|
| 883 |
margin-bottom: var(--space-4);
|
| 884 |
+
position: relative;
|
| 885 |
+
z-index: 1;
|
| 886 |
}
|
| 887 |
|
| 888 |
.result-header h3 {
|
| 889 |
font-size: var(--font-size-lg);
|
| 890 |
+
font-weight: 700;
|
| 891 |
color: var(--text-strong);
|
| 892 |
margin: 0;
|
| 893 |
}
|
|
|
|
| 895 |
.result-time {
|
| 896 |
font-size: var(--font-size-xs);
|
| 897 |
color: var(--text-muted);
|
| 898 |
+
padding: 6px 10px;
|
| 899 |
+
border-radius: var(--radius-full);
|
| 900 |
+
background: rgba(255, 255, 255, 0.06);
|
| 901 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
.result-body {
|
| 905 |
+
display: grid;
|
| 906 |
+
grid-template-columns: minmax(260px, 360px) 1fr;
|
| 907 |
+
gap: var(--space-5);
|
| 908 |
+
position: relative;
|
| 909 |
+
z-index: 1;
|
| 910 |
}
|
| 911 |
|
| 912 |
.sentiment-display {
|
| 913 |
+
--accent-rgb: 96 165 250; /* default */
|
| 914 |
+
text-align: left;
|
| 915 |
padding: var(--space-6);
|
| 916 |
+
background: linear-gradient(135deg, rgba(var(--accent-rgb) / 0.14) 0%, rgba(255, 255, 255, 0.04) 65%);
|
| 917 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 918 |
border-radius: var(--radius-xl);
|
| 919 |
+
display: grid;
|
| 920 |
+
grid-template-columns: 76px 1fr;
|
| 921 |
+
grid-template-rows: auto auto;
|
| 922 |
+
column-gap: var(--space-4);
|
| 923 |
+
row-gap: var(--space-2);
|
| 924 |
}
|
| 925 |
|
| 926 |
+
.sentiment-display[data-sentiment="bullish"],
|
| 927 |
+
.sentiment-display[data-sentiment="positive"],
|
| 928 |
+
.sentiment-display[data-sentiment="buy"] { --accent-rgb: 34 197 94; }
|
| 929 |
+
|
| 930 |
+
.sentiment-display[data-sentiment="bearish"],
|
| 931 |
+
.sentiment-display[data-sentiment="negative"],
|
| 932 |
+
.sentiment-display[data-sentiment="sell"] { --accent-rgb: 239 68 68; }
|
| 933 |
+
|
| 934 |
+
.sentiment-display[data-sentiment="neutral"],
|
| 935 |
+
.sentiment-display[data-sentiment="hold"] { --accent-rgb: 59 130 246; }
|
| 936 |
+
|
| 937 |
+
.sentiment-display[data-sentiment="unknown"] { --accent-rgb: 148 163 184; }
|
| 938 |
+
|
| 939 |
.sentiment-emoji {
|
| 940 |
+
grid-row: 1 / span 2;
|
| 941 |
+
grid-column: 1;
|
| 942 |
+
width: 76px;
|
| 943 |
+
height: 76px;
|
| 944 |
+
display: grid;
|
| 945 |
+
place-items: center;
|
| 946 |
+
font-size: 42px;
|
| 947 |
+
border-radius: 18px;
|
| 948 |
+
background: rgba(var(--accent-rgb) / 0.16);
|
| 949 |
+
border: 1px solid rgba(var(--accent-rgb) / 0.28);
|
| 950 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
| 951 |
}
|
| 952 |
|
| 953 |
.sentiment-label {
|
| 954 |
+
grid-column: 2;
|
| 955 |
+
grid-row: 1;
|
| 956 |
font-family: 'Space Grotesk', sans-serif;
|
| 957 |
+
font-size: clamp(22px, 2.2vw, 30px);
|
| 958 |
font-weight: 700;
|
| 959 |
text-transform: uppercase;
|
| 960 |
+
letter-spacing: 0.08em;
|
| 961 |
+
margin: 0;
|
| 962 |
+
line-height: 1.1;
|
| 963 |
+
color: rgb(var(--accent-rgb));
|
| 964 |
}
|
| 965 |
|
| 966 |
+
.sentiment-label.bullish,
|
| 967 |
+
.sentiment-label.positive,
|
| 968 |
+
.sentiment-label.buy { color: rgb(34, 197, 94); }
|
| 969 |
+
|
| 970 |
+
.sentiment-label.bearish,
|
| 971 |
+
.sentiment-label.negative,
|
| 972 |
+
.sentiment-label.sell { color: rgb(239, 68, 68); }
|
| 973 |
+
|
| 974 |
+
.sentiment-label.neutral,
|
| 975 |
+
.sentiment-label.hold { color: rgb(59, 130, 246); }
|
| 976 |
+
|
| 977 |
+
.sentiment-label.unknown { color: rgb(148, 163, 184); }
|
| 978 |
|
| 979 |
.sentiment-confidence {
|
| 980 |
+
grid-column: 2;
|
| 981 |
+
grid-row: 2;
|
| 982 |
+
font-size: var(--font-size-sm);
|
| 983 |
+
color: rgba(226, 232, 240, 0.85);
|
| 984 |
+
padding: 10px 12px;
|
| 985 |
+
border-radius: var(--radius-lg);
|
| 986 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 987 |
+
background:
|
| 988 |
+
linear-gradient(
|
| 989 |
+
90deg,
|
| 990 |
+
rgba(var(--accent-rgb) / 0.25) 0 var(--confidence, 0%),
|
| 991 |
+
rgba(255, 255, 255, 0.06) var(--confidence, 0%) 100%
|
| 992 |
+
);
|
| 993 |
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
| 994 |
}
|
| 995 |
|
| 996 |
.result-details {
|
| 997 |
+
background: rgba(0, 0, 0, 0.38);
|
| 998 |
+
border: 1px solid rgba(255, 255, 255, 0.10);
|
| 999 |
border-radius: var(--radius-md);
|
| 1000 |
padding: var(--space-4);
|
| 1001 |
overflow: auto;
|
|
|
|
| 1004 |
|
| 1005 |
.result-json {
|
| 1006 |
font-family: 'JetBrains Mono', monospace;
|
| 1007 |
+
font-size: 12px;
|
| 1008 |
+
color: rgba(226, 232, 240, 0.92);
|
| 1009 |
white-space: pre-wrap;
|
| 1010 |
margin: 0;
|
| 1011 |
+
line-height: 1.55;
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
@media (max-width: 900px) {
|
| 1015 |
+
.result-body {
|
| 1016 |
+
grid-template-columns: 1fr;
|
| 1017 |
+
}
|
| 1018 |
+
.sentiment-display {
|
| 1019 |
+
grid-template-columns: 64px 1fr;
|
| 1020 |
+
padding: var(--space-5);
|
| 1021 |
+
}
|
| 1022 |
+
.sentiment-emoji {
|
| 1023 |
+
width: 64px;
|
| 1024 |
+
height: 64px;
|
| 1025 |
+
font-size: 36px;
|
| 1026 |
+
border-radius: 16px;
|
| 1027 |
+
}
|
| 1028 |
}
|
| 1029 |
|
| 1030 |
/* =========================================================================
|
|
@@ -11,6 +11,8 @@ import logger from '../../shared/js/utils/logger.js';
|
|
| 11 |
class ModelsPage {
|
| 12 |
constructor() {
|
| 13 |
this.models = [];
|
|
|
|
|
|
|
| 14 |
this.refreshInterval = null;
|
| 15 |
}
|
| 16 |
|
|
@@ -20,6 +22,7 @@ class ModelsPage {
|
|
| 20 |
|
| 21 |
this.bindEvents();
|
| 22 |
await this.loadModels();
|
|
|
|
| 23 |
|
| 24 |
this.refreshInterval = setInterval(() => this.loadModels(), 60000);
|
| 25 |
|
|
@@ -30,6 +33,16 @@ class ModelsPage {
|
|
| 30 |
}
|
| 31 |
}
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
bindEvents() {
|
| 34 |
// Refresh button
|
| 35 |
const refreshBtn = document.getElementById('refresh-btn');
|
|
@@ -81,6 +94,23 @@ class ModelsPage {
|
|
| 81 |
this.reinitializeAll();
|
| 82 |
});
|
| 83 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
}
|
| 85 |
|
| 86 |
switchTab(tabId) {
|
|
@@ -131,7 +161,7 @@ class ModelsPage {
|
|
| 131 |
const response = await fetch('/api/models/list', {
|
| 132 |
method: 'GET',
|
| 133 |
headers: { 'Content-Type': 'application/json' },
|
| 134 |
-
signal:
|
| 135 |
});
|
| 136 |
|
| 137 |
if (response.ok) {
|
|
@@ -154,7 +184,7 @@ class ModelsPage {
|
|
| 154 |
const response = await fetch('/api/models/status', {
|
| 155 |
method: 'GET',
|
| 156 |
headers: { 'Content-Type': 'application/json' },
|
| 157 |
-
signal:
|
| 158 |
});
|
| 159 |
|
| 160 |
if (response.ok) {
|
|
@@ -179,7 +209,7 @@ class ModelsPage {
|
|
| 179 |
const response = await fetch('/api/models/summary', {
|
| 180 |
method: 'GET',
|
| 181 |
headers: { 'Content-Type': 'application/json' },
|
| 182 |
-
signal:
|
| 183 |
});
|
| 184 |
|
| 185 |
if (response.ok) {
|
|
@@ -232,7 +262,9 @@ class ModelsPage {
|
|
| 232 |
this.models = this.getFallbackModels();
|
| 233 |
}
|
| 234 |
|
| 235 |
-
this.
|
|
|
|
|
|
|
| 236 |
|
| 237 |
// Update stats from payload or calculate from models
|
| 238 |
const stats = {
|
|
@@ -258,7 +290,9 @@ class ModelsPage {
|
|
| 258 |
|
| 259 |
// Fallback to demo data
|
| 260 |
this.models = this.getFallbackModels();
|
| 261 |
-
this.
|
|
|
|
|
|
|
| 262 |
this.renderStats({
|
| 263 |
total_models: this.models.length,
|
| 264 |
models_loaded: 0,
|
|
@@ -274,19 +308,38 @@ class ModelsPage {
|
|
| 274 |
populateTestModelSelect() {
|
| 275 |
const testModelSelect = document.getElementById('test-model-select');
|
| 276 |
if (testModelSelect && this.models.length > 0) {
|
| 277 |
-
|
|
|
|
| 278 |
|
| 279 |
-
this.models.
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
}
|
| 286 |
});
|
| 287 |
}
|
| 288 |
}
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
/**
|
| 291 |
* Extract models array from various payload structures
|
| 292 |
*/
|
|
@@ -458,9 +511,12 @@ class ModelsPage {
|
|
| 458 |
method: 'POST',
|
| 459 |
headers: { 'Content-Type': 'application/json' },
|
| 460 |
body: JSON.stringify({
|
| 461 |
-
text: 'Bitcoin is going to the moon! 🚀'
|
|
|
|
|
|
|
|
|
|
| 462 |
}),
|
| 463 |
-
signal:
|
| 464 |
});
|
| 465 |
|
| 466 |
if (response.ok) {
|
|
@@ -501,7 +557,7 @@ class ModelsPage {
|
|
| 501 |
}
|
| 502 |
|
| 503 |
const text = input.value.trim();
|
| 504 |
-
const
|
| 505 |
|
| 506 |
this.showToast('Analyzing...', 'info');
|
| 507 |
|
|
@@ -509,8 +565,13 @@ class ModelsPage {
|
|
| 509 |
const response = await fetch('/api/sentiment/analyze', {
|
| 510 |
method: 'POST',
|
| 511 |
headers: { 'Content-Type': 'application/json' },
|
| 512 |
-
body: JSON.stringify({
|
| 513 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
});
|
| 515 |
|
| 516 |
if (!response.ok) {
|
|
@@ -529,13 +590,26 @@ class ModelsPage {
|
|
| 529 |
const emojiEl = document.getElementById('sentiment-emoji');
|
| 530 |
const labelEl = document.getElementById('sentiment-label');
|
| 531 |
const confidenceEl = document.getElementById('sentiment-confidence');
|
|
|
|
| 532 |
const timeEl = document.getElementById('result-time');
|
| 533 |
const jsonPre = document.querySelector('.result-json');
|
| 534 |
|
| 535 |
if (emojiEl) emojiEl.textContent = emoji;
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
if (confidenceEl) {
|
| 538 |
-
|
|
|
|
| 539 |
}
|
| 540 |
if (timeEl) timeEl.textContent = new Date().toLocaleTimeString();
|
| 541 |
if (jsonPre) jsonPre.textContent = JSON.stringify(result, null, 2);
|
|
@@ -547,6 +621,111 @@ class ModelsPage {
|
|
| 547 |
}
|
| 548 |
}
|
| 549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
getSentimentEmoji(sentiment) {
|
| 551 |
const emojiMap = {
|
| 552 |
'positive': '😊',
|
|
@@ -581,7 +760,7 @@ class ModelsPage {
|
|
| 581 |
const response = await fetch('/api/models/reinitialize', {
|
| 582 |
method: 'POST',
|
| 583 |
headers: { 'Content-Type': 'application/json' },
|
| 584 |
-
signal:
|
| 585 |
});
|
| 586 |
|
| 587 |
if (response.ok) {
|
|
|
|
| 11 |
class ModelsPage {
|
| 12 |
constructor() {
|
| 13 |
this.models = [];
|
| 14 |
+
this.allModels = [];
|
| 15 |
+
this.activeFilters = { category: 'all', status: 'all' };
|
| 16 |
this.refreshInterval = null;
|
| 17 |
}
|
| 18 |
|
|
|
|
| 22 |
|
| 23 |
this.bindEvents();
|
| 24 |
await this.loadModels();
|
| 25 |
+
await this.loadHealth();
|
| 26 |
|
| 27 |
this.refreshInterval = setInterval(() => this.loadModels(), 60000);
|
| 28 |
|
|
|
|
| 33 |
}
|
| 34 |
}
|
| 35 |
|
| 36 |
+
createTimeoutSignal(ms = 10000) {
|
| 37 |
+
// Prefer AbortSignal.timeout when available, fallback to AbortController.
|
| 38 |
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
| 39 |
+
return AbortSignal.timeout(ms);
|
| 40 |
+
}
|
| 41 |
+
const controller = new AbortController();
|
| 42 |
+
setTimeout(() => controller.abort(), ms);
|
| 43 |
+
return controller.signal;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
bindEvents() {
|
| 47 |
// Refresh button
|
| 48 |
const refreshBtn = document.getElementById('refresh-btn');
|
|
|
|
| 94 |
this.reinitializeAll();
|
| 95 |
});
|
| 96 |
}
|
| 97 |
+
|
| 98 |
+
// Filters
|
| 99 |
+
const categoryFilter = document.getElementById('category-filter');
|
| 100 |
+
if (categoryFilter) {
|
| 101 |
+
categoryFilter.addEventListener('change', (e) => {
|
| 102 |
+
this.activeFilters.category = e.target.value || 'all';
|
| 103 |
+
this.applyFilters();
|
| 104 |
+
});
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const statusFilter = document.getElementById('status-filter');
|
| 108 |
+
if (statusFilter) {
|
| 109 |
+
statusFilter.addEventListener('change', (e) => {
|
| 110 |
+
this.activeFilters.status = e.target.value || 'all';
|
| 111 |
+
this.applyFilters();
|
| 112 |
+
});
|
| 113 |
+
}
|
| 114 |
}
|
| 115 |
|
| 116 |
switchTab(tabId) {
|
|
|
|
| 161 |
const response = await fetch('/api/models/list', {
|
| 162 |
method: 'GET',
|
| 163 |
headers: { 'Content-Type': 'application/json' },
|
| 164 |
+
signal: this.createTimeoutSignal(10000)
|
| 165 |
});
|
| 166 |
|
| 167 |
if (response.ok) {
|
|
|
|
| 184 |
const response = await fetch('/api/models/status', {
|
| 185 |
method: 'GET',
|
| 186 |
headers: { 'Content-Type': 'application/json' },
|
| 187 |
+
signal: this.createTimeoutSignal(10000)
|
| 188 |
});
|
| 189 |
|
| 190 |
if (response.ok) {
|
|
|
|
| 209 |
const response = await fetch('/api/models/summary', {
|
| 210 |
method: 'GET',
|
| 211 |
headers: { 'Content-Type': 'application/json' },
|
| 212 |
+
signal: this.createTimeoutSignal(10000)
|
| 213 |
});
|
| 214 |
|
| 215 |
if (response.ok) {
|
|
|
|
| 262 |
this.models = this.getFallbackModels();
|
| 263 |
}
|
| 264 |
|
| 265 |
+
this.allModels = [...this.models];
|
| 266 |
+
this.applyFilters(false);
|
| 267 |
+
this.renderCatalog();
|
| 268 |
|
| 269 |
// Update stats from payload or calculate from models
|
| 270 |
const stats = {
|
|
|
|
| 290 |
|
| 291 |
// Fallback to demo data
|
| 292 |
this.models = this.getFallbackModels();
|
| 293 |
+
this.allModels = [...this.models];
|
| 294 |
+
this.applyFilters(false);
|
| 295 |
+
this.renderCatalog();
|
| 296 |
this.renderStats({
|
| 297 |
total_models: this.models.length,
|
| 298 |
models_loaded: 0,
|
|
|
|
| 308 |
populateTestModelSelect() {
|
| 309 |
const testModelSelect = document.getElementById('test-model-select');
|
| 310 |
if (testModelSelect && this.models.length > 0) {
|
| 311 |
+
// Allow testing any model key via backend (auto-fallback if unavailable)
|
| 312 |
+
testModelSelect.innerHTML = '<option value="">Auto (best available)</option>';
|
| 313 |
|
| 314 |
+
const sorted = [...this.models].sort((a, b) => (a.category || '').localeCompare(b.category || '') || (a.name || '').localeCompare(b.name || ''));
|
| 315 |
+
sorted.forEach(model => {
|
| 316 |
+
const option = document.createElement('option');
|
| 317 |
+
option.value = model.key;
|
| 318 |
+
option.textContent = `${model.name} (${model.category})`;
|
| 319 |
+
testModelSelect.appendChild(option);
|
|
|
|
| 320 |
});
|
| 321 |
}
|
| 322 |
}
|
| 323 |
|
| 324 |
+
applyFilters(shouldRerender = true) {
|
| 325 |
+
const category = this.activeFilters.category;
|
| 326 |
+
const status = this.activeFilters.status;
|
| 327 |
+
|
| 328 |
+
const filtered = (this.allModels || []).filter((m) => {
|
| 329 |
+
const catOk = category === 'all' ? true : (m.category === category || (m.category || '').toLowerCase() === category.toLowerCase());
|
| 330 |
+
const statusOk = status === 'all' ? true : (m.status === status || (status === 'available' && !m.loaded && !m.failed));
|
| 331 |
+
return catOk && statusOk;
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
this.models = filtered;
|
| 335 |
+
if (shouldRerender) {
|
| 336 |
+
this.renderModels();
|
| 337 |
+
} else {
|
| 338 |
+
// For initial load path we still need to render once.
|
| 339 |
+
this.renderModels();
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
/**
|
| 344 |
* Extract models array from various payload structures
|
| 345 |
*/
|
|
|
|
| 511 |
method: 'POST',
|
| 512 |
headers: { 'Content-Type': 'application/json' },
|
| 513 |
body: JSON.stringify({
|
| 514 |
+
text: 'Bitcoin is going to the moon! 🚀',
|
| 515 |
+
mode: 'crypto',
|
| 516 |
+
model_key: modelId,
|
| 517 |
+
use_ensemble: false
|
| 518 |
}),
|
| 519 |
+
signal: this.createTimeoutSignal(10000)
|
| 520 |
});
|
| 521 |
|
| 522 |
if (response.ok) {
|
|
|
|
| 557 |
}
|
| 558 |
|
| 559 |
const text = input.value.trim();
|
| 560 |
+
const modelKey = modelSelect?.value || '';
|
| 561 |
|
| 562 |
this.showToast('Analyzing...', 'info');
|
| 563 |
|
|
|
|
| 565 |
const response = await fetch('/api/sentiment/analyze', {
|
| 566 |
method: 'POST',
|
| 567 |
headers: { 'Content-Type': 'application/json' },
|
| 568 |
+
body: JSON.stringify({
|
| 569 |
+
text,
|
| 570 |
+
mode: 'crypto',
|
| 571 |
+
model_key: modelKey || undefined,
|
| 572 |
+
use_ensemble: !modelKey
|
| 573 |
+
}),
|
| 574 |
+
signal: this.createTimeoutSignal(10000)
|
| 575 |
});
|
| 576 |
|
| 577 |
if (!response.ok) {
|
|
|
|
| 590 |
const emojiEl = document.getElementById('sentiment-emoji');
|
| 591 |
const labelEl = document.getElementById('sentiment-label');
|
| 592 |
const confidenceEl = document.getElementById('sentiment-confidence');
|
| 593 |
+
const displayEl = document.getElementById('sentiment-display');
|
| 594 |
const timeEl = document.getElementById('result-time');
|
| 595 |
const jsonPre = document.querySelector('.result-json');
|
| 596 |
|
| 597 |
if (emojiEl) emojiEl.textContent = emoji;
|
| 598 |
+
const sentimentKey = (result.sentiment || 'unknown').toString().toLowerCase();
|
| 599 |
+
if (displayEl) {
|
| 600 |
+
displayEl.setAttribute('data-sentiment', sentimentKey);
|
| 601 |
+
const pct = (typeof result.score === 'number' ? result.score : 0) * 100;
|
| 602 |
+
displayEl.style.setProperty('--confidence', `${Math.max(0, Math.min(100, pct)).toFixed(1)}%`);
|
| 603 |
+
}
|
| 604 |
+
if (labelEl) {
|
| 605 |
+
labelEl.textContent = result.sentiment || 'Unknown';
|
| 606 |
+
// Ensure CSS sentiment variants can apply reliably
|
| 607 |
+
labelEl.classList.remove('bullish', 'bearish', 'neutral', 'positive', 'negative', 'buy', 'sell', 'hold', 'unknown');
|
| 608 |
+
labelEl.classList.add(sentimentKey);
|
| 609 |
+
}
|
| 610 |
if (confidenceEl) {
|
| 611 |
+
const pct = (typeof result.score === 'number' ? result.score : 0) * 100;
|
| 612 |
+
confidenceEl.textContent = `Confidence: ${Math.max(0, Math.min(100, pct)).toFixed(1)}%`;
|
| 613 |
}
|
| 614 |
if (timeEl) timeEl.textContent = new Date().toLocaleTimeString();
|
| 615 |
if (jsonPre) jsonPre.textContent = JSON.stringify(result, null, 2);
|
|
|
|
| 621 |
}
|
| 622 |
}
|
| 623 |
|
| 624 |
+
async loadHealth() {
|
| 625 |
+
const container = document.getElementById('health-grid');
|
| 626 |
+
if (!container) return;
|
| 627 |
+
|
| 628 |
+
container.innerHTML = `
|
| 629 |
+
<div class="loading-state">
|
| 630 |
+
<div class="loading-spinner"></div>
|
| 631 |
+
<p class="loading-text">Loading health data...</p>
|
| 632 |
+
</div>
|
| 633 |
+
`;
|
| 634 |
+
|
| 635 |
+
try {
|
| 636 |
+
const res = await fetch('/api/models/health', {
|
| 637 |
+
method: 'GET',
|
| 638 |
+
headers: { 'Content-Type': 'application/json' },
|
| 639 |
+
signal: this.createTimeoutSignal(10000)
|
| 640 |
+
});
|
| 641 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 642 |
+
const data = await res.json();
|
| 643 |
+
|
| 644 |
+
const health = Array.isArray(data.health) ? data.health : (data.health ? Object.values(data.health) : []);
|
| 645 |
+
if (!health.length) {
|
| 646 |
+
container.innerHTML = `
|
| 647 |
+
<div class="empty-state glass-card" style="grid-column: 1 / -1;">
|
| 648 |
+
<div class="empty-icon">🏥</div>
|
| 649 |
+
<h3>No health data</h3>
|
| 650 |
+
<p>Health registry is empty (models may be running in fallback mode).</p>
|
| 651 |
+
</div>
|
| 652 |
+
`;
|
| 653 |
+
return;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
container.innerHTML = health.map((h) => {
|
| 657 |
+
const status = h.status || 'unknown';
|
| 658 |
+
const statusClass = status === 'healthy' ? 'loaded' : status === 'unavailable' ? 'failed' : 'available';
|
| 659 |
+
const name = h.name || h.key || 'model';
|
| 660 |
+
return `
|
| 661 |
+
<div class="model-card ${statusClass}">
|
| 662 |
+
<div class="model-header">
|
| 663 |
+
<div class="model-info">
|
| 664 |
+
<h3 class="model-name">${name}</h3>
|
| 665 |
+
<p class="model-type">Health: ${status}</p>
|
| 666 |
+
</div>
|
| 667 |
+
<div class="model-status ${statusClass}">${status}</div>
|
| 668 |
+
</div>
|
| 669 |
+
<div class="model-body">
|
| 670 |
+
<div class="model-meta">
|
| 671 |
+
<span class="meta-badge">✅ ${Number(h.success_count || 0)} success</span>
|
| 672 |
+
<span class="meta-badge">⚠️ ${Number(h.error_count || 0)} errors</span>
|
| 673 |
+
</div>
|
| 674 |
+
</div>
|
| 675 |
+
</div>
|
| 676 |
+
`;
|
| 677 |
+
}).join('');
|
| 678 |
+
} catch (e) {
|
| 679 |
+
container.innerHTML = `
|
| 680 |
+
<div class="empty-state glass-card" style="grid-column: 1 / -1;">
|
| 681 |
+
<div class="empty-icon">⚠️</div>
|
| 682 |
+
<h3>Health data unavailable</h3>
|
| 683 |
+
<p>${e?.message || 'Unable to fetch /api/models/health'}</p>
|
| 684 |
+
</div>
|
| 685 |
+
`;
|
| 686 |
+
}
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
renderCatalog() {
|
| 690 |
+
// Best-effort catalog fill; only runs if the catalog containers exist on this page.
|
| 691 |
+
const buckets = {
|
| 692 |
+
crypto: document.getElementById('catalog-crypto'),
|
| 693 |
+
financial: document.getElementById('catalog-financial'),
|
| 694 |
+
social: document.getElementById('catalog-social'),
|
| 695 |
+
trading: document.getElementById('catalog-trading'),
|
| 696 |
+
generation: document.getElementById('catalog-generation'),
|
| 697 |
+
summarization: document.getElementById('catalog-summarization')
|
| 698 |
+
};
|
| 699 |
+
|
| 700 |
+
const hasAny = Object.values(buckets).some(Boolean);
|
| 701 |
+
if (!hasAny) return;
|
| 702 |
+
|
| 703 |
+
const byBucket = { crypto: [], financial: [], social: [], trading: [], generation: [], summarization: [] };
|
| 704 |
+
(this.allModels || []).forEach((m) => {
|
| 705 |
+
const cat = (m.category || '').toLowerCase();
|
| 706 |
+
if (cat.includes('crypto')) byBucket.crypto.push(m);
|
| 707 |
+
else if (cat.includes('financial')) byBucket.financial.push(m);
|
| 708 |
+
else if (cat.includes('social')) byBucket.social.push(m);
|
| 709 |
+
else if (cat.includes('trading')) byBucket.trading.push(m);
|
| 710 |
+
else if (cat.includes('generation') || cat.includes('gen')) byBucket.generation.push(m);
|
| 711 |
+
else if (cat.includes('summar')) byBucket.summarization.push(m);
|
| 712 |
+
});
|
| 713 |
+
|
| 714 |
+
const renderList = (list) => list.map((m) => `
|
| 715 |
+
<div class="catalog-model-item">
|
| 716 |
+
<div class="catalog-model-name">${m.name}</div>
|
| 717 |
+
<div class="catalog-model-id">${m.model_id}</div>
|
| 718 |
+
</div>
|
| 719 |
+
`).join('') || '<div class="empty-state"><p>No models in this category.</p></div>';
|
| 720 |
+
|
| 721 |
+
if (buckets.crypto) buckets.crypto.innerHTML = renderList(byBucket.crypto);
|
| 722 |
+
if (buckets.financial) buckets.financial.innerHTML = renderList(byBucket.financial);
|
| 723 |
+
if (buckets.social) buckets.social.innerHTML = renderList(byBucket.social);
|
| 724 |
+
if (buckets.trading) buckets.trading.innerHTML = renderList(byBucket.trading);
|
| 725 |
+
if (buckets.generation) buckets.generation.innerHTML = renderList(byBucket.generation);
|
| 726 |
+
if (buckets.summarization) buckets.summarization.innerHTML = renderList(byBucket.summarization);
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
getSentimentEmoji(sentiment) {
|
| 730 |
const emojiMap = {
|
| 731 |
'positive': '😊',
|
|
|
|
| 760 |
const response = await fetch('/api/models/reinitialize', {
|
| 761 |
method: 'POST',
|
| 762 |
headers: { 'Content-Type': 'application/json' },
|
| 763 |
+
signal: this.createTimeoutSignal(30000)
|
| 764 |
});
|
| 765 |
|
| 766 |
if (response.ok) {
|
|
@@ -23,6 +23,29 @@ export class ConfigHelperModal {
|
|
| 23 |
const baseUrl = window.location.origin;
|
| 24 |
|
| 25 |
return [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
// ===== UNIFIED SERVICE API =====
|
| 27 |
{
|
| 28 |
name: 'Unified Service API',
|
|
@@ -31,11 +54,12 @@ export class ConfigHelperModal {
|
|
| 31 |
endpoints: [
|
| 32 |
{ method: 'GET', path: '/api/service/rate?pair=BTC/USDT', desc: 'Get exchange rate' },
|
| 33 |
{ method: 'GET', path: '/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT', desc: 'Multiple rates' },
|
|
|
|
| 34 |
{ method: 'GET', path: '/api/service/market-status', desc: 'Market overview' },
|
| 35 |
{ method: 'GET', path: '/api/service/top?n=10', desc: 'Top cryptocurrencies' },
|
| 36 |
{ method: 'GET', path: '/api/service/sentiment?symbol=BTC', desc: 'Get sentiment' },
|
| 37 |
-
{ method: 'GET', path: '/api/service/whales?chain=ethereum&min_amount_usd=1000000', desc: 'Whale transactions' },
|
| 38 |
-
{ method: 'GET', path: '/api/service/onchain?address=0x...&chain=ethereum', desc: 'On-chain data' },
|
| 39 |
{ method: 'POST', path: '/api/service/query', desc: 'Universal query endpoint' }
|
| 40 |
],
|
| 41 |
example: `// Get BTC price
|
|
@@ -53,21 +77,22 @@ fetch('${baseUrl}/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT,BNB/USDT')
|
|
| 53 |
{
|
| 54 |
name: 'Market Data API',
|
| 55 |
category: 'Market Data',
|
| 56 |
-
description: 'Real-time prices, OHLCV, and market statistics
|
| 57 |
endpoints: [
|
| 58 |
{ method: 'GET', path: '/api/market?limit=100', desc: 'Market data with prices' },
|
| 59 |
-
{ method: 'GET', path: '/api/ohlcv?symbol=BTC&timeframe=1h&limit=500', desc: 'OHLCV candlestick data' },
|
| 60 |
-
{ method: 'GET', path: '/api/klines?symbol=BTCUSDT&interval=1h', desc: 'Klines (alias for OHLCV)' },
|
| 61 |
-
{ method: 'GET', path: '/api/historical?symbol=BTC&days=30', desc: 'Historical price data' },
|
| 62 |
{ method: 'GET', path: '/api/coins/top?limit=50', desc: 'Top coins by market cap' },
|
| 63 |
-
{ method: 'GET', path: '/api/trending', desc: 'Trending cryptocurrencies' }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
],
|
| 65 |
example: `// Get OHLCV data for charting
|
| 66 |
fetch('${baseUrl}/api/ohlcv?symbol=BTC&timeframe=1h&limit=100')
|
| 67 |
.then(res => res.json())
|
| 68 |
.then(data => {
|
| 69 |
console.log('OHLCV data:', data.data);
|
| 70 |
-
// Each candle: {
|
| 71 |
});`
|
| 72 |
},
|
| 73 |
|
|
@@ -77,12 +102,11 @@ fetch('${baseUrl}/api/ohlcv?symbol=BTC&timeframe=1h&limit=100')
|
|
| 77 |
category: 'News & Media',
|
| 78 |
description: 'Crypto news from 9+ sources including RSS feeds',
|
| 79 |
endpoints: [
|
| 80 |
-
{ method: 'GET', path: '/api/news?limit=20', desc: 'Latest crypto news' },
|
| 81 |
-
{ method: 'GET', path: '/api/news
|
| 82 |
-
{ method: 'GET', path: '/api/news?source=decrypt', desc: 'News from specific source' }
|
| 83 |
],
|
| 84 |
example: `// Get latest news
|
| 85 |
-
fetch('${baseUrl}/api/news?limit=10')
|
| 86 |
.then(res => res.json())
|
| 87 |
.then(data => {
|
| 88 |
data.articles.forEach(article => {
|
|
@@ -98,7 +122,7 @@ fetch('${baseUrl}/api/news?limit=10')
|
|
| 98 |
description: 'Fear & Greed Index, social sentiment, and AI-powered analysis',
|
| 99 |
endpoints: [
|
| 100 |
{ method: 'GET', path: '/api/sentiment/global', desc: 'Global market sentiment' },
|
| 101 |
-
{ method: 'GET', path: '/api/fear-greed', desc: 'Fear & Greed Index' },
|
| 102 |
{ method: 'GET', path: '/api/sentiment/asset/{symbol}', desc: 'Asset-specific sentiment' },
|
| 103 |
{ method: 'POST', path: '/api/sentiment/analyze', desc: 'Analyze custom text' }
|
| 104 |
],
|
|
@@ -113,10 +137,10 @@ fetch('${baseUrl}/api/fear-greed')
|
|
| 113 |
fetch('${baseUrl}/api/sentiment/analyze', {
|
| 114 |
method: 'POST',
|
| 115 |
headers: { 'Content-Type': 'application/json' },
|
| 116 |
-
body: JSON.stringify({ text: 'Bitcoin is going to the moon!' })
|
| 117 |
})
|
| 118 |
.then(res => res.json())
|
| 119 |
-
.then(data => console.log('Sentiment:', data.
|
| 120 |
},
|
| 121 |
|
| 122 |
// ===== ON-CHAIN ANALYTICS =====
|
|
@@ -125,19 +149,13 @@ fetch('${baseUrl}/api/sentiment/analyze', {
|
|
| 125 |
category: 'Analytics',
|
| 126 |
description: 'Blockchain data, whale tracking, and network statistics',
|
| 127 |
endpoints: [
|
| 128 |
-
{ method: 'GET', path: '/api/
|
| 129 |
-
{ method: 'GET', path: '/api/
|
| 130 |
-
{ method: 'GET', path: '/api/whales/stats?hours=24', desc: 'Whale activity statistics' },
|
| 131 |
-
{ method: 'GET', path: '/api/blockchain/gas?chain=ethereum', desc: 'Gas prices' }
|
| 132 |
],
|
| 133 |
example: `// Get whale transactions
|
| 134 |
fetch('${baseUrl}/api/service/whales?chain=ethereum&min_amount_usd=1000000&limit=20')
|
| 135 |
.then(res => res.json())
|
| 136 |
-
.then(data =>
|
| 137 |
-
data.data.forEach(tx => {
|
| 138 |
-
console.log('Whale:', tx.amount_usd, 'USD', tx.chain);
|
| 139 |
-
});
|
| 140 |
-
});`
|
| 141 |
},
|
| 142 |
|
| 143 |
// ===== TECHNICAL ANALYSIS =====
|
|
@@ -172,6 +190,28 @@ fetch('${baseUrl}/api/technical/ta-quick', {
|
|
| 172 |
console.log('Entry Range:', data.entry_range);
|
| 173 |
});`
|
| 174 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
// ===== AI MODELS =====
|
| 177 |
{
|
|
@@ -181,15 +221,16 @@ fetch('${baseUrl}/api/technical/ta-quick', {
|
|
| 181 |
endpoints: [
|
| 182 |
{ method: 'GET', path: '/api/models/status', desc: 'Models status' },
|
| 183 |
{ method: 'GET', path: '/api/models/list', desc: 'List all models' },
|
|
|
|
| 184 |
{ method: 'GET', path: '/api/models/health', desc: 'Model health check' },
|
| 185 |
-
{ method: 'POST', path: '/api/models/
|
| 186 |
{ method: 'POST', path: '/api/ai/decision', desc: 'AI trading decision' }
|
| 187 |
],
|
| 188 |
example: `// Get AI trading decision
|
| 189 |
fetch('${baseUrl}/api/ai/decision', {
|
| 190 |
method: 'POST',
|
| 191 |
headers: { 'Content-Type': 'application/json' },
|
| 192 |
-
body: JSON.stringify({ symbol: 'BTC',
|
| 193 |
})
|
| 194 |
.then(res => res.json())
|
| 195 |
.then(data => {
|
|
@@ -203,21 +244,36 @@ fetch('${baseUrl}/api/ai/decision', {
|
|
| 203 |
{
|
| 204 |
name: 'DeFi Data API',
|
| 205 |
category: 'DeFi Services',
|
| 206 |
-
description: 'DefiLlama TVL, protocols, yields
|
| 207 |
endpoints: [
|
| 208 |
{ method: 'GET', path: '/api/defi/tvl', desc: 'Total Value Locked' },
|
| 209 |
{ method: 'GET', path: '/api/defi/protocols?limit=20', desc: 'Top DeFi protocols' },
|
| 210 |
-
{ method: 'GET', path: '/api/defi/yields', desc: 'DeFi
|
| 211 |
],
|
| 212 |
example: `// Get DeFi TVL data
|
| 213 |
fetch('${baseUrl}/api/defi/protocols?limit=10')
|
| 214 |
.then(res => res.json())
|
| 215 |
.then(data => {
|
| 216 |
-
data.protocols.forEach(p => {
|
| 217 |
console.log(p.name, '- TVL:', p.tvl);
|
| 218 |
});
|
| 219 |
});`
|
| 220 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
// ===== RESOURCES & MONITORING =====
|
| 223 |
{
|
|
@@ -248,39 +304,23 @@ fetch('${baseUrl}/api/resources/stats')
|
|
| 248 |
console.log('Success Rate:', data.success_rate + '%');
|
| 249 |
});`
|
| 250 |
},
|
| 251 |
-
|
| 252 |
-
// =====
|
| 253 |
{
|
| 254 |
-
name: '
|
| 255 |
-
category: '
|
| 256 |
-
description: '
|
| 257 |
endpoints: [
|
| 258 |
-
{ method: '
|
| 259 |
-
{ method: '
|
| 260 |
-
{ method: '
|
| 261 |
-
{ method: '
|
|
|
|
| 262 |
],
|
| 263 |
-
example: `//
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
ws.send(JSON.stringify({
|
| 268 |
-
action: 'subscribe',
|
| 269 |
-
service: 'market_data'
|
| 270 |
-
}));
|
| 271 |
-
};
|
| 272 |
-
|
| 273 |
-
ws.onmessage = (event) => {
|
| 274 |
-
const data = JSON.parse(event.data);
|
| 275 |
-
console.log('Real-time update:', data);
|
| 276 |
-
};
|
| 277 |
-
|
| 278 |
-
// Alternative: HTTP polling (recommended)
|
| 279 |
-
setInterval(async () => {
|
| 280 |
-
const data = await fetch('${baseUrl}/api/market?limit=100')
|
| 281 |
-
.then(r => r.json());
|
| 282 |
-
console.log('Market data:', data);
|
| 283 |
-
}, 30000);`
|
| 284 |
}
|
| 285 |
];
|
| 286 |
}
|
|
@@ -324,7 +364,7 @@ setInterval(async () => {
|
|
| 324 |
|
| 325 |
<div class="config-helper-body">
|
| 326 |
<div class="config-helper-intro">
|
| 327 |
-
<p>
|
| 328 |
<div class="config-helper-base-url">
|
| 329 |
<strong>Base URL:</strong>
|
| 330 |
<code>${window.location.origin}</code>
|
|
@@ -336,10 +376,44 @@ setInterval(async () => {
|
|
| 336 |
</button>
|
| 337 |
</div>
|
| 338 |
<div class="config-helper-stats">
|
| 339 |
-
<span
|
| 340 |
-
<span
|
| 341 |
-
<span
|
| 342 |
-
<span
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
</div>
|
| 344 |
</div>
|
| 345 |
|
|
@@ -358,7 +432,12 @@ setInterval(async () => {
|
|
| 358 |
modal.querySelectorAll('.copy-btn').forEach(btn => {
|
| 359 |
btn.addEventListener('click', (e) => {
|
| 360 |
e.stopPropagation();
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
this.copyToClipboard(text, btn);
|
| 363 |
});
|
| 364 |
});
|
|
@@ -450,7 +529,20 @@ setInterval(async () => {
|
|
| 450 |
|
| 451 |
async copyToClipboard(text, button) {
|
| 452 |
try {
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
|
| 455 |
// Visual feedback
|
| 456 |
const originalHTML = button.innerHTML;
|
|
@@ -609,6 +701,42 @@ style.textContent = `
|
|
| 609 |
border-radius: 4px;
|
| 610 |
}
|
| 611 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
.service-category {
|
| 613 |
margin-bottom: 24px;
|
| 614 |
}
|
|
@@ -806,6 +934,10 @@ style.textContent = `
|
|
| 806 |
.config-helper-stats {
|
| 807 |
flex-direction: column;
|
| 808 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 809 |
}
|
| 810 |
`;
|
| 811 |
document.head.appendChild(style);
|
|
|
|
| 23 |
const baseUrl = window.location.origin;
|
| 24 |
|
| 25 |
return [
|
| 26 |
+
// ===== QUICK DISCOVERY =====
|
| 27 |
+
{
|
| 28 |
+
name: 'Discovery & Health',
|
| 29 |
+
category: 'Getting Started',
|
| 30 |
+
description: 'Verify the server is online and discover all available endpoints',
|
| 31 |
+
endpoints: [
|
| 32 |
+
{ method: 'GET', path: '/api/health', desc: 'Health check' },
|
| 33 |
+
{ method: 'GET', path: '/api/status', desc: 'System status' },
|
| 34 |
+
{ method: 'GET', path: '/api/routers', desc: 'Loaded routers status' },
|
| 35 |
+
{ method: 'GET', path: '/api/endpoints', desc: 'Full endpoints list (grouped)' },
|
| 36 |
+
{ method: 'GET', path: '/docs', desc: 'Swagger UI documentation' }
|
| 37 |
+
],
|
| 38 |
+
example: `// Health check
|
| 39 |
+
fetch('${baseUrl}/api/health')
|
| 40 |
+
.then(res => res.json())
|
| 41 |
+
.then(console.log);
|
| 42 |
+
|
| 43 |
+
// Get full endpoints list
|
| 44 |
+
fetch('${baseUrl}/api/endpoints')
|
| 45 |
+
.then(res => res.json())
|
| 46 |
+
.then(data => console.log('Total:', data.total_endpoints));`
|
| 47 |
+
},
|
| 48 |
+
|
| 49 |
// ===== UNIFIED SERVICE API =====
|
| 50 |
{
|
| 51 |
name: 'Unified Service API',
|
|
|
|
| 54 |
endpoints: [
|
| 55 |
{ method: 'GET', path: '/api/service/rate?pair=BTC/USDT', desc: 'Get exchange rate' },
|
| 56 |
{ method: 'GET', path: '/api/service/rate/batch?pairs=BTC/USDT,ETH/USDT', desc: 'Multiple rates' },
|
| 57 |
+
{ method: 'GET', path: '/api/service/history?symbol=BTC&interval=60&limit=200', desc: 'Historical OHLC (minutes interval)' },
|
| 58 |
{ method: 'GET', path: '/api/service/market-status', desc: 'Market overview' },
|
| 59 |
{ method: 'GET', path: '/api/service/top?n=10', desc: 'Top cryptocurrencies' },
|
| 60 |
{ method: 'GET', path: '/api/service/sentiment?symbol=BTC', desc: 'Get sentiment' },
|
| 61 |
+
{ method: 'GET', path: '/api/service/whales?chain=ethereum&min_amount_usd=1000000', desc: 'Whale transactions (service)' },
|
| 62 |
+
{ method: 'GET', path: '/api/service/onchain?address=0x...&chain=ethereum', desc: 'On-chain data (service)' },
|
| 63 |
{ method: 'POST', path: '/api/service/query', desc: 'Universal query endpoint' }
|
| 64 |
],
|
| 65 |
example: `// Get BTC price
|
|
|
|
| 77 |
{
|
| 78 |
name: 'Market Data API',
|
| 79 |
category: 'Market Data',
|
| 80 |
+
description: 'Real-time prices, OHLC/OHLCV, and market statistics',
|
| 81 |
endpoints: [
|
| 82 |
{ method: 'GET', path: '/api/market?limit=100', desc: 'Market data with prices' },
|
|
|
|
|
|
|
|
|
|
| 83 |
{ method: 'GET', path: '/api/coins/top?limit=50', desc: 'Top coins by market cap' },
|
| 84 |
+
{ method: 'GET', path: '/api/trending', desc: 'Trending cryptocurrencies' },
|
| 85 |
+
{ method: 'GET', path: '/api/market/ohlc?symbol=BTC&timeframe=1h', desc: 'OHLC (multi-source, recommended)' },
|
| 86 |
+
{ method: 'GET', path: '/api/ohlcv?symbol=BTC&timeframe=1h&limit=100', desc: 'OHLCV (query-style)' },
|
| 87 |
+
{ method: 'GET', path: '/api/klines?symbol=BTCUSDT&interval=1h&limit=100', desc: 'Klines alias (Binance style)' },
|
| 88 |
+
{ method: 'GET', path: '/api/historical?symbol=BTC&days=30', desc: 'Daily historical candles (alias)' }
|
| 89 |
],
|
| 90 |
example: `// Get OHLCV data for charting
|
| 91 |
fetch('${baseUrl}/api/ohlcv?symbol=BTC&timeframe=1h&limit=100')
|
| 92 |
.then(res => res.json())
|
| 93 |
.then(data => {
|
| 94 |
console.log('OHLCV data:', data.data);
|
| 95 |
+
// Each candle: { timestamp, open, high, low, close, volume }
|
| 96 |
});`
|
| 97 |
},
|
| 98 |
|
|
|
|
| 102 |
category: 'News & Media',
|
| 103 |
description: 'Crypto news from 9+ sources including RSS feeds',
|
| 104 |
endpoints: [
|
| 105 |
+
{ method: 'GET', path: '/api/news/latest?limit=20', desc: 'Latest crypto news' },
|
| 106 |
+
{ method: 'GET', path: '/api/news?limit=20', desc: 'Alias for latest (compat)' }
|
|
|
|
| 107 |
],
|
| 108 |
example: `// Get latest news
|
| 109 |
+
fetch('${baseUrl}/api/news/latest?limit=10')
|
| 110 |
.then(res => res.json())
|
| 111 |
.then(data => {
|
| 112 |
data.articles.forEach(article => {
|
|
|
|
| 122 |
description: 'Fear & Greed Index, social sentiment, and AI-powered analysis',
|
| 123 |
endpoints: [
|
| 124 |
{ method: 'GET', path: '/api/sentiment/global', desc: 'Global market sentiment' },
|
| 125 |
+
{ method: 'GET', path: '/api/fear-greed', desc: 'Fear & Greed Index (alias)' },
|
| 126 |
{ method: 'GET', path: '/api/sentiment/asset/{symbol}', desc: 'Asset-specific sentiment' },
|
| 127 |
{ method: 'POST', path: '/api/sentiment/analyze', desc: 'Analyze custom text' }
|
| 128 |
],
|
|
|
|
| 137 |
fetch('${baseUrl}/api/sentiment/analyze', {
|
| 138 |
method: 'POST',
|
| 139 |
headers: { 'Content-Type': 'application/json' },
|
| 140 |
+
body: JSON.stringify({ text: 'Bitcoin is going to the moon!', mode: 'crypto' })
|
| 141 |
})
|
| 142 |
.then(res => res.json())
|
| 143 |
+
.then(data => console.log('Sentiment:', data.sentiment, data.score));`
|
| 144 |
},
|
| 145 |
|
| 146 |
// ===== ON-CHAIN ANALYTICS =====
|
|
|
|
| 149 |
category: 'Analytics',
|
| 150 |
description: 'Blockchain data, whale tracking, and network statistics',
|
| 151 |
endpoints: [
|
| 152 |
+
{ method: 'GET', path: '/api/service/whales?chain=ethereum&min_amount_usd=1000000&limit=20', desc: 'Whale transactions (service)' },
|
| 153 |
+
{ method: 'GET', path: '/api/service/onchain?address=0x...&chain=ethereum', desc: 'On-chain snapshot (service)' }
|
|
|
|
|
|
|
| 154 |
],
|
| 155 |
example: `// Get whale transactions
|
| 156 |
fetch('${baseUrl}/api/service/whales?chain=ethereum&min_amount_usd=1000000&limit=20')
|
| 157 |
.then(res => res.json())
|
| 158 |
+
.then(data => console.log(data));`
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
},
|
| 160 |
|
| 161 |
// ===== TECHNICAL ANALYSIS =====
|
|
|
|
| 190 |
console.log('Entry Range:', data.entry_range);
|
| 191 |
});`
|
| 192 |
},
|
| 193 |
+
|
| 194 |
+
// ===== INDICATORS =====
|
| 195 |
+
{
|
| 196 |
+
name: 'Indicator Services API',
|
| 197 |
+
category: 'Analysis Services',
|
| 198 |
+
description: 'Technical indicators (RSI, MACD, SMA/EMA, BB, ATR, StochRSI) + comprehensive signals',
|
| 199 |
+
endpoints: [
|
| 200 |
+
{ method: 'GET', path: '/api/indicators/services', desc: 'List available indicator services' },
|
| 201 |
+
{ method: 'GET', path: '/api/indicators/rsi?symbol=BTC&timeframe=1h&period=14', desc: 'RSI' },
|
| 202 |
+
{ method: 'GET', path: '/api/indicators/macd?symbol=BTC&timeframe=1h', desc: 'MACD' },
|
| 203 |
+
{ method: 'GET', path: '/api/indicators/comprehensive?symbol=BTC&timeframe=1h', desc: 'Comprehensive indicator analysis' }
|
| 204 |
+
],
|
| 205 |
+
example: `// List indicator services
|
| 206 |
+
fetch('${baseUrl}/api/indicators/services')
|
| 207 |
+
.then(r => r.json())
|
| 208 |
+
.then(console.log);
|
| 209 |
+
|
| 210 |
+
// RSI
|
| 211 |
+
fetch('${baseUrl}/api/indicators/rsi?symbol=BTC&timeframe=1h&period=14')
|
| 212 |
+
.then(r => r.json())
|
| 213 |
+
.then(console.log);`
|
| 214 |
+
},
|
| 215 |
|
| 216 |
// ===== AI MODELS =====
|
| 217 |
{
|
|
|
|
| 221 |
endpoints: [
|
| 222 |
{ method: 'GET', path: '/api/models/status', desc: 'Models status' },
|
| 223 |
{ method: 'GET', path: '/api/models/list', desc: 'List all models' },
|
| 224 |
+
{ method: 'GET', path: '/api/models/summary', desc: 'Models grouped by category (frontend-ready)' },
|
| 225 |
{ method: 'GET', path: '/api/models/health', desc: 'Model health check' },
|
| 226 |
+
{ method: 'POST', path: '/api/models/reinitialize', desc: 'Reinitialize models (UI button)' },
|
| 227 |
{ method: 'POST', path: '/api/ai/decision', desc: 'AI trading decision' }
|
| 228 |
],
|
| 229 |
example: `// Get AI trading decision
|
| 230 |
fetch('${baseUrl}/api/ai/decision', {
|
| 231 |
method: 'POST',
|
| 232 |
headers: { 'Content-Type': 'application/json' },
|
| 233 |
+
body: JSON.stringify({ symbol: 'BTC', horizon: 'swing', risk_tolerance: 'moderate' })
|
| 234 |
})
|
| 235 |
.then(res => res.json())
|
| 236 |
.then(data => {
|
|
|
|
| 244 |
{
|
| 245 |
name: 'DeFi Data API',
|
| 246 |
category: 'DeFi Services',
|
| 247 |
+
description: 'DefiLlama public endpoints (no API key): TVL, protocols, yields',
|
| 248 |
endpoints: [
|
| 249 |
{ method: 'GET', path: '/api/defi/tvl', desc: 'Total Value Locked' },
|
| 250 |
{ method: 'GET', path: '/api/defi/protocols?limit=20', desc: 'Top DeFi protocols' },
|
| 251 |
+
{ method: 'GET', path: '/api/defi/yields?limit=20', desc: 'DeFi yield pools' }
|
| 252 |
],
|
| 253 |
example: `// Get DeFi TVL data
|
| 254 |
fetch('${baseUrl}/api/defi/protocols?limit=10')
|
| 255 |
.then(res => res.json())
|
| 256 |
.then(data => {
|
| 257 |
+
(data.protocols || []).forEach(p => {
|
| 258 |
console.log(p.name, '- TVL:', p.tvl);
|
| 259 |
});
|
| 260 |
});`
|
| 261 |
},
|
| 262 |
+
|
| 263 |
+
// ===== TRADING & BACKTESTING =====
|
| 264 |
+
{
|
| 265 |
+
name: 'Trading & Backtesting API',
|
| 266 |
+
category: 'Trading',
|
| 267 |
+
description: 'Historical backtests and strategy runs (uses exchange/data fallbacks)',
|
| 268 |
+
endpoints: [
|
| 269 |
+
{ method: 'GET', path: '/api/trading/backtest/historical/BTCUSDT?timeframe=1h&days=30', desc: 'Historical candles for backtest' },
|
| 270 |
+
{ method: 'GET', path: '/api/trading/backtest/run/BTCUSDT?strategy=sma_crossover&days=30&initial_capital=10000', desc: 'Run a backtest strategy' }
|
| 271 |
+
],
|
| 272 |
+
example: `// Run SMA crossover backtest
|
| 273 |
+
fetch('${baseUrl}/api/trading/backtest/run/BTCUSDT?strategy=sma_crossover&days=30&initial_capital=10000')
|
| 274 |
+
.then(r => r.json())
|
| 275 |
+
.then(console.log);`
|
| 276 |
+
},
|
| 277 |
|
| 278 |
// ===== RESOURCES & MONITORING =====
|
| 279 |
{
|
|
|
|
| 304 |
console.log('Success Rate:', data.success_rate + '%');
|
| 305 |
});`
|
| 306 |
},
|
| 307 |
+
|
| 308 |
+
// ===== SUPPORT / DEBUG =====
|
| 309 |
{
|
| 310 |
+
name: 'Support & Debug API',
|
| 311 |
+
category: 'System Services',
|
| 312 |
+
description: 'Client-accessible support files and real endpoint list',
|
| 313 |
endpoints: [
|
| 314 |
+
{ method: 'GET', path: '/api/support/realendpoints', desc: 'Endpoints list (JSON)' },
|
| 315 |
+
{ method: 'GET', path: '/api/support/realendpoints?format=txt', desc: 'Endpoints list (TXT)' },
|
| 316 |
+
{ method: 'GET', path: '/realendpoint.txt', desc: 'Download endpoints list (TXT)' },
|
| 317 |
+
{ method: 'GET', path: '/api/support/fualt?tail=200', desc: 'Tail of fault log (JSON)' },
|
| 318 |
+
{ method: 'GET', path: '/fualt.txt', desc: 'Download full fault log (TXT)' }
|
| 319 |
],
|
| 320 |
+
example: `// Fetch tail of fualt.txt
|
| 321 |
+
fetch('${baseUrl}/api/support/fualt?tail=200')
|
| 322 |
+
.then(r => r.json())
|
| 323 |
+
.then(data => console.log(data.content));`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
}
|
| 325 |
];
|
| 326 |
}
|
|
|
|
| 364 |
|
| 365 |
<div class="config-helper-body">
|
| 366 |
<div class="config-helper-intro">
|
| 367 |
+
<p>Use this guide to integrate quickly. Every endpoint listed below exists in the server and is safe to copy.</p>
|
| 368 |
<div class="config-helper-base-url">
|
| 369 |
<strong>Base URL:</strong>
|
| 370 |
<code>${window.location.origin}</code>
|
|
|
|
| 376 |
</button>
|
| 377 |
</div>
|
| 378 |
<div class="config-helper-stats">
|
| 379 |
+
<span>✅ Copy-ready URLs</span>
|
| 380 |
+
<span>✅ Copy-ready code snippets</span>
|
| 381 |
+
<span>✅ `/api/endpoints` for discovery</span>
|
| 382 |
+
<span>✅ `/docs` for Swagger</span>
|
| 383 |
+
</div>
|
| 384 |
+
|
| 385 |
+
<div class="config-helper-snippets">
|
| 386 |
+
<div class="snippet-card">
|
| 387 |
+
<div class="snippet-head">
|
| 388 |
+
<span>cURL</span>
|
| 389 |
+
<button class="copy-btn">Copy</button>
|
| 390 |
+
</div>
|
| 391 |
+
<pre><code>curl -s '${window.location.origin}/api/health' | jq .</code></pre>
|
| 392 |
+
</div>
|
| 393 |
+
<div class="snippet-card">
|
| 394 |
+
<div class="snippet-head">
|
| 395 |
+
<span>JavaScript (fetch)</span>
|
| 396 |
+
<button class="copy-btn">Copy</button>
|
| 397 |
+
</div>
|
| 398 |
+
<pre><code>fetch('${window.location.origin}/api/market?limit=10')
|
| 399 |
+
.then(r => r.json())
|
| 400 |
+
.then(console.log);</code></pre>
|
| 401 |
+
</div>
|
| 402 |
+
<div class="snippet-card">
|
| 403 |
+
<div class="snippet-head">
|
| 404 |
+
<span>Python (requests)</span>
|
| 405 |
+
<button class="copy-btn">Copy</button>
|
| 406 |
+
</div>
|
| 407 |
+
<pre><code>import requests
|
| 408 |
+
print(requests.get('${window.location.origin}/api/market?limit=10', timeout=10).json())</code></pre>
|
| 409 |
+
</div>
|
| 410 |
+
<div class="snippet-card">
|
| 411 |
+
<div class="snippet-head">
|
| 412 |
+
<span>Optional HF Token</span>
|
| 413 |
+
<button class="copy-btn">Copy</button>
|
| 414 |
+
</div>
|
| 415 |
+
<pre><code>export HF_TOKEN='YOUR_TOKEN_HERE'</code></pre>
|
| 416 |
+
</div>
|
| 417 |
</div>
|
| 418 |
</div>
|
| 419 |
|
|
|
|
| 432 |
modal.querySelectorAll('.copy-btn').forEach(btn => {
|
| 433 |
btn.addEventListener('click', (e) => {
|
| 434 |
e.stopPropagation();
|
| 435 |
+
let text = btn.getAttribute('data-copy');
|
| 436 |
+
if (!text) {
|
| 437 |
+
// Snippet cards copy their visible code block
|
| 438 |
+
const codeEl = btn.closest('.snippet-card')?.querySelector('pre code');
|
| 439 |
+
text = codeEl?.textContent || '';
|
| 440 |
+
}
|
| 441 |
this.copyToClipboard(text, btn);
|
| 442 |
});
|
| 443 |
});
|
|
|
|
| 529 |
|
| 530 |
async copyToClipboard(text, button) {
|
| 531 |
try {
|
| 532 |
+
if (navigator.clipboard?.writeText) {
|
| 533 |
+
await navigator.clipboard.writeText(text);
|
| 534 |
+
} else {
|
| 535 |
+
// Fallback for older browsers / restricted contexts
|
| 536 |
+
const ta = document.createElement('textarea');
|
| 537 |
+
ta.value = text;
|
| 538 |
+
ta.setAttribute('readonly', '');
|
| 539 |
+
ta.style.position = 'fixed';
|
| 540 |
+
ta.style.left = '-9999px';
|
| 541 |
+
document.body.appendChild(ta);
|
| 542 |
+
ta.select();
|
| 543 |
+
document.execCommand('copy');
|
| 544 |
+
ta.remove();
|
| 545 |
+
}
|
| 546 |
|
| 547 |
// Visual feedback
|
| 548 |
const originalHTML = button.innerHTML;
|
|
|
|
| 701 |
border-radius: 4px;
|
| 702 |
}
|
| 703 |
|
| 704 |
+
.config-helper-snippets {
|
| 705 |
+
display: grid;
|
| 706 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 707 |
+
gap: 12px;
|
| 708 |
+
margin-top: 12px;
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
.snippet-card {
|
| 712 |
+
background: var(--bg-secondary, #f8fdfc);
|
| 713 |
+
border: 1px solid var(--border-light, #e5e7eb);
|
| 714 |
+
border-radius: 12px;
|
| 715 |
+
padding: 12px;
|
| 716 |
+
overflow: hidden;
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
.snippet-head {
|
| 720 |
+
display: flex;
|
| 721 |
+
align-items: center;
|
| 722 |
+
justify-content: space-between;
|
| 723 |
+
font-size: 12px;
|
| 724 |
+
font-weight: 700;
|
| 725 |
+
color: var(--text-secondary, #6b7280);
|
| 726 |
+
margin-bottom: 8px;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
.snippet-card pre {
|
| 730 |
+
margin: 0;
|
| 731 |
+
background: #0b1220;
|
| 732 |
+
color: #e2e8f0;
|
| 733 |
+
padding: 10px;
|
| 734 |
+
border-radius: 10px;
|
| 735 |
+
overflow-x: auto;
|
| 736 |
+
font-size: 12px;
|
| 737 |
+
line-height: 1.5;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
.service-category {
|
| 741 |
margin-bottom: 24px;
|
| 742 |
}
|
|
|
|
| 934 |
.config-helper-stats {
|
| 935 |
flex-direction: column;
|
| 936 |
}
|
| 937 |
+
|
| 938 |
+
.config-helper-snippets {
|
| 939 |
+
grid-template-columns: 1fr;
|
| 940 |
+
}
|
| 941 |
}
|
| 942 |
`;
|
| 943 |
document.head.appendChild(style);
|
|
@@ -231,3 +231,42 @@ input::placeholder {
|
|
| 231 |
clip: rect(0, 0, 0, 0);
|
| 232 |
border: 0;
|
| 233 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
clip: rect(0, 0, 0, 0);
|
| 232 |
border: 0;
|
| 233 |
}
|
| 234 |
+
|
| 235 |
+
/* ==========================================================================
|
| 236 |
+
Disable "blinking" (looping) animations globally
|
| 237 |
+
--------------------------------------------------------------------------
|
| 238 |
+
Many pages/components use infinite pulse/shimmer/gradient animations that can
|
| 239 |
+
look like blinking. We stop ALL looping animations across the app, but keep
|
| 240 |
+
loading spinners rotating so users still get feedback during async loads.
|
| 241 |
+
========================================================================== */
|
| 242 |
+
|
| 243 |
+
/* Stop all looping animations (including on pseudo-elements) */
|
| 244 |
+
html body *,
|
| 245 |
+
html body *::before,
|
| 246 |
+
html body *::after {
|
| 247 |
+
animation-iteration-count: 1 !important;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* Keep true loading spinners rotating */
|
| 251 |
+
html body .spinner,
|
| 252 |
+
html body .loading-spinner,
|
| 253 |
+
html body .spinner-icon,
|
| 254 |
+
html body .spinner::before,
|
| 255 |
+
html body .spinner::after,
|
| 256 |
+
html body .loading-spinner::before,
|
| 257 |
+
html body .loading-spinner::after,
|
| 258 |
+
html body .spinner-icon::before,
|
| 259 |
+
html body .spinner-icon::after {
|
| 260 |
+
animation-iteration-count: infinite !important;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* Explicitly disable LIVE badge blinking (header/sidebar). Some pages load
|
| 264 |
+
global.css async; this hard-stop prevents any repeated pulsing. */
|
| 265 |
+
html body .live-badge,
|
| 266 |
+
html body .live-dot,
|
| 267 |
+
html body .live-pulse,
|
| 268 |
+
html body .live-badge-enhanced,
|
| 269 |
+
html body .live-badge-enhanced .live-pulse,
|
| 270 |
+
html body .live-badge-enhanced .live-text {
|
| 271 |
+
animation: none !important;
|
| 272 |
+
}
|
|
@@ -5,10 +5,22 @@
|
|
| 5 |
|
| 6 |
import { CONFIG } from '../core/config.js';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
export class Toast {
|
| 9 |
static container = null;
|
| 10 |
static toasts = [];
|
| 11 |
-
static maxToasts =
|
| 12 |
|
| 13 |
/**
|
| 14 |
* Initialize toast container
|
|
@@ -35,7 +47,9 @@ export class Toast {
|
|
| 35 |
id: Date.now() + Math.random(),
|
| 36 |
message,
|
| 37 |
type,
|
| 38 |
-
duration:
|
|
|
|
|
|
|
| 39 |
dismissible: options.dismissible !== false,
|
| 40 |
action: options.action || null,
|
| 41 |
};
|
|
|
|
| 5 |
|
| 6 |
import { CONFIG } from '../core/config.js';
|
| 7 |
|
| 8 |
+
const TOAST_DEFAULTS = {
|
| 9 |
+
MAX_VISIBLE: 3,
|
| 10 |
+
DEFAULT_DURATION: 3500,
|
| 11 |
+
ERROR_DURATION: 6000,
|
| 12 |
+
};
|
| 13 |
+
|
| 14 |
+
// CONFIG.TOAST is optional in some builds/pages; keep Toast resilient.
|
| 15 |
+
const TOAST_CONFIG = {
|
| 16 |
+
...TOAST_DEFAULTS,
|
| 17 |
+
...(CONFIG?.TOAST || {}),
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
export class Toast {
|
| 21 |
static container = null;
|
| 22 |
static toasts = [];
|
| 23 |
+
static maxToasts = TOAST_CONFIG.MAX_VISIBLE;
|
| 24 |
|
| 25 |
/**
|
| 26 |
* Initialize toast container
|
|
|
|
| 47 |
id: Date.now() + Math.random(),
|
| 48 |
message,
|
| 49 |
type,
|
| 50 |
+
duration:
|
| 51 |
+
options.duration ??
|
| 52 |
+
(type === 'error' ? TOAST_CONFIG.ERROR_DURATION : TOAST_CONFIG.DEFAULT_DURATION),
|
| 53 |
dismissible: options.dismissible !== false,
|
| 54 |
action: options.action || null,
|
| 55 |
};
|
|
@@ -77,6 +77,10 @@ export const API_ENDPOINTS = {
|
|
| 77 |
resourcesCategory: '/api/resources/category',
|
| 78 |
resourcesApis: '/api/resources/apis',
|
| 79 |
providers: '/api/providers',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
// Advanced
|
| 82 |
multiSourceData: '/api/multi-source/data',
|
|
@@ -204,6 +208,12 @@ export const CONFIG = {
|
|
| 204 |
MAX_RETRIES: 3,
|
| 205 |
RETRY_DELAY: 1000,
|
| 206 |
RETRIES: 3,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
IS_HUGGINGFACE: IS_HUGGINGFACE,
|
| 208 |
IS_LOCALHOST: IS_LOCALHOST,
|
| 209 |
ENVIRONMENT: IS_HUGGINGFACE ? 'huggingface' : IS_LOCALHOST ? 'local' : 'production'
|
|
|
|
| 77 |
resourcesCategory: '/api/resources/category',
|
| 78 |
resourcesApis: '/api/resources/apis',
|
| 79 |
providers: '/api/providers',
|
| 80 |
+
|
| 81 |
+
// Support / Debug (client-consumable)
|
| 82 |
+
supportFualt: '/api/support/fualt',
|
| 83 |
+
supportRealEndpoints: '/api/support/realendpoints',
|
| 84 |
|
| 85 |
// Advanced
|
| 86 |
multiSourceData: '/api/multi-source/data',
|
|
|
|
| 208 |
MAX_RETRIES: 3,
|
| 209 |
RETRY_DELAY: 1000,
|
| 210 |
RETRIES: 3,
|
| 211 |
+
// UI defaults (used by shared components like toast)
|
| 212 |
+
TOAST: {
|
| 213 |
+
MAX_VISIBLE: 3,
|
| 214 |
+
DEFAULT_DURATION: 3500,
|
| 215 |
+
ERROR_DURATION: 6000
|
| 216 |
+
},
|
| 217 |
IS_HUGGINGFACE: IS_HUGGINGFACE,
|
| 218 |
IS_LOCALHOST: IS_LOCALHOST,
|
| 219 |
ENVIRONMENT: IS_HUGGINGFACE ? 'huggingface' : IS_LOCALHOST ? 'local' : 'production'
|
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Support Client
|
| 3 |
+
* Client-facing helpers for fualt.txt + realendpoint.txt support endpoints.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { API_ENDPOINTS } from './config.js';
|
| 7 |
+
|
| 8 |
+
export class SupportClient {
|
| 9 |
+
static async getFualt({ tail = 500 } = {}) {
|
| 10 |
+
const url = `${API_ENDPOINTS.supportFualt}?tail=${encodeURIComponent(tail)}`;
|
| 11 |
+
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
|
| 12 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 13 |
+
return await res.json();
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
static async getRealEndpoints({ format = 'json' } = {}) {
|
| 17 |
+
const url = `${API_ENDPOINTS.supportRealEndpoints}?format=${encodeURIComponent(format)}`;
|
| 18 |
+
const res = await fetch(url, { headers: { 'Content-Type': 'application/json' } });
|
| 19 |
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
| 20 |
+
if (format === 'txt') return await res.text();
|
| 21 |
+
return await res.json();
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export default SupportClient;
|
| 26 |
+
|