Spaces:
Running
Running
Upload hub_dashboard_service.py
Browse files- hub_dashboard_service.py +137 -19
hub_dashboard_service.py
CHANGED
|
@@ -27,18 +27,9 @@ from pathlib import Path
|
|
| 27 |
from typing import Dict, List, Optional
|
| 28 |
|
| 29 |
import websocket
|
| 30 |
-
from flask import Flask, jsonify, request, send_from_directory
|
| 31 |
from flask_cors import CORS
|
| 32 |
|
| 33 |
-
# ββ Ranker Logs Blueprint βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
-
# Import lazily so the service starts even if ranker_logs_api.py is absent.
|
| 35 |
-
try:
|
| 36 |
-
from ranker_logs_api import ranker_logs_bp, init_ranker_logs_api as _init_bp
|
| 37 |
-
_RANKER_LOGS_BP_AVAILABLE = True
|
| 38 |
-
except ImportError:
|
| 39 |
-
_RANKER_LOGS_BP_AVAILABLE = False
|
| 40 |
-
logger_import_warning = "ranker_logs_api.py not found β Blueprint endpoints disabled"
|
| 41 |
-
|
| 42 |
# ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
logging.basicConfig(
|
| 44 |
level=logging.INFO,
|
|
@@ -701,14 +692,139 @@ _hub_subscriber.start()
|
|
| 701 |
app = Flask(__name__)
|
| 702 |
CORS(app)
|
| 703 |
|
| 704 |
-
# ββ
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
|
| 713 |
|
| 714 |
@app.route("/")
|
|
@@ -767,10 +883,12 @@ def health():
|
|
| 767 |
|
| 768 |
|
| 769 |
if __name__ == "__main__":
|
| 770 |
-
logger.info("=== K1RL QUASAR HUB DASHBOARD SERVICE v2.
|
| 771 |
logger.info(f"Dashboard: http://localhost:{_DASHBOARD_PORT}")
|
| 772 |
logger.info(f"Log directory: {_LOG_DIR}")
|
| 773 |
logger.info("Fixes applied:")
|
|
|
|
|
|
|
| 774 |
logger.info(" β
FIX #1: Now reading *.log* (includes rotated files)")
|
| 775 |
logger.info(" β
FIX #2: Improved regex to catch all trade close formats")
|
| 776 |
logger.info(" β
FIX #4: Dedicated exit_price regex (no optional-group ambiguity)")
|
|
|
|
| 27 |
from typing import Dict, List, Optional
|
| 28 |
|
| 29 |
import websocket
|
| 30 |
+
from flask import Flask, jsonify, request, send_from_directory, send_file
|
| 31 |
from flask_cors import CORS
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# ββ Logging βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 34 |
logging.basicConfig(
|
| 35 |
level=logging.INFO,
|
|
|
|
| 692 |
app = Flask(__name__)
|
| 693 |
CORS(app)
|
| 694 |
|
| 695 |
+
# ββ Instantiate the file-based log adapter (used by all /api/ranker/logs/* routes) ββ
|
| 696 |
+
_log_adapter = FileBasedLoggerAdapter(log_dir=_LOG_DIR)
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 700 |
+
# SECTION 4 β RANKER LOG ROUTES (self-contained β no Blueprint dependency)
|
| 701 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 702 |
+
#
|
| 703 |
+
# FIX v2.4: These routes were previously delegated to ranker_logs_api.py Blueprint.
|
| 704 |
+
# That Blueprint was never registered, so every /api/ranker/logs/* call returned 404.
|
| 705 |
+
# Routes are now defined inline so hub_dashboard_service.py is fully self-contained.
|
| 706 |
+
# FileBasedLoggerAdapter (above) satisfies the full RankerLogger interface by reading
|
| 707 |
+
# the ranker's disk log files β no in-process ranker instance required.
|
| 708 |
+
|
| 709 |
+
_TRAINING_RE_INLINE = re.compile(
|
| 710 |
+
r'step=(\d+)\s*\|\s*loss=([\d.]+)\s*\|\s*lr=([\d.eE+\-]+)\s*\|\s*assets=(\d+)'
|
| 711 |
+
)
|
| 712 |
+
_JSON_BLOB_RE_INLINE = re.compile(r'(\{.*\})\s*$')
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def _enrich_training(entry: dict) -> dict:
|
| 716 |
+
"""Attach parsed `data` dict to TRAINING entries so dashboard KPI cards populate."""
|
| 717 |
+
if entry.get("category", "").upper() != "TRAINING":
|
| 718 |
+
return entry
|
| 719 |
+
if entry.get("data"):
|
| 720 |
+
return entry
|
| 721 |
+
msg = entry.get("message", "")
|
| 722 |
+
m = _TRAINING_RE_INLINE.search(msg)
|
| 723 |
+
if m:
|
| 724 |
+
entry["data"] = {
|
| 725 |
+
"step": int(m.group(1)),
|
| 726 |
+
"loss": float(m.group(2)),
|
| 727 |
+
"lr": float(m.group(3)),
|
| 728 |
+
"asset_count": int(m.group(4)),
|
| 729 |
+
}
|
| 730 |
+
return entry
|
| 731 |
+
jm = _JSON_BLOB_RE_INLINE.search(msg)
|
| 732 |
+
if jm:
|
| 733 |
+
try:
|
| 734 |
+
blob = json.loads(jm.group(1))
|
| 735 |
+
if "step" in blob:
|
| 736 |
+
entry["data"] = {
|
| 737 |
+
"step": blob.get("step", 0),
|
| 738 |
+
"loss": blob.get("loss", 0.0),
|
| 739 |
+
"lr": blob.get("lr", 0.0),
|
| 740 |
+
"asset_count": blob.get("asset_count", blob.get("assets", 0)),
|
| 741 |
+
}
|
| 742 |
+
except (ValueError, KeyError):
|
| 743 |
+
pass
|
| 744 |
+
return entry
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
@app.route("/api/ranker/logs/recent", methods=["GET"])
|
| 748 |
+
def api_logs_recent():
|
| 749 |
+
"""GET /api/ranker/logs/recent?limit=50&category=TRAINING"""
|
| 750 |
+
try:
|
| 751 |
+
limit = int(request.args.get("limit", 50))
|
| 752 |
+
category = request.args.get("category")
|
| 753 |
+
entries = _log_adapter.get_recent(n=limit, category=category)
|
| 754 |
+
entries = [_enrich_training(e) for e in entries]
|
| 755 |
+
return jsonify({
|
| 756 |
+
"logs": entries,
|
| 757 |
+
"count": len(entries),
|
| 758 |
+
"stats": _log_adapter.get_stats(),
|
| 759 |
+
})
|
| 760 |
+
except Exception as exc:
|
| 761 |
+
logger.exception(f"[api_logs_recent] error: {exc}")
|
| 762 |
+
return jsonify({"logs": [], "count": 0, "error": str(exc)}), 200
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
@app.route("/api/ranker/logs/stats", methods=["GET"])
|
| 766 |
+
def api_logs_stats():
|
| 767 |
+
"""GET /api/ranker/logs/stats"""
|
| 768 |
+
try:
|
| 769 |
+
return jsonify(_log_adapter.get_stats())
|
| 770 |
+
except Exception as exc:
|
| 771 |
+
logger.exception(f"[api_logs_stats] error: {exc}")
|
| 772 |
+
return jsonify({"total_events": 0, "by_level": {}, "by_category": {},
|
| 773 |
+
"by_asset": {}, "errors": {}, "error": str(exc)}), 200
|
| 774 |
+
|
| 775 |
+
|
| 776 |
+
@app.route("/api/ranker/logs/asset/<asset>", methods=["GET"])
|
| 777 |
+
def api_logs_asset(asset: str):
|
| 778 |
+
"""GET /api/ranker/logs/asset/V75?limit=30"""
|
| 779 |
+
try:
|
| 780 |
+
limit = int(request.args.get("limit", 30))
|
| 781 |
+
entries = _log_adapter.get_by_asset(asset, n=limit)
|
| 782 |
+
return jsonify({"asset": asset, "logs": entries, "count": len(entries)})
|
| 783 |
+
except Exception as exc:
|
| 784 |
+
logger.exception(f"[api_logs_asset] error: {exc}")
|
| 785 |
+
return jsonify({"asset": asset, "logs": [], "count": 0, "error": str(exc)}), 200
|
| 786 |
+
|
| 787 |
+
|
| 788 |
+
@app.route("/api/ranker/logs/level/<level>", methods=["GET"])
|
| 789 |
+
def api_logs_level(level: str):
|
| 790 |
+
"""GET /api/ranker/logs/level/ERROR?limit=50"""
|
| 791 |
+
try:
|
| 792 |
+
limit = int(request.args.get("limit", 50))
|
| 793 |
+
entries = _log_adapter.get_by_level(level, n=limit)
|
| 794 |
+
return jsonify({"level": level.upper(), "logs": entries, "count": len(entries)})
|
| 795 |
+
except Exception as exc:
|
| 796 |
+
logger.exception(f"[api_logs_level] error: {exc}")
|
| 797 |
+
return jsonify({"level": level.upper(), "logs": [], "count": 0, "error": str(exc)}), 200
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
@app.route("/api/ranker/logs/export", methods=["GET"])
|
| 801 |
+
def api_logs_export():
|
| 802 |
+
"""GET /api/ranker/logs/export?limit=500 β download JSON"""
|
| 803 |
+
try:
|
| 804 |
+
limit = int(request.args.get("limit", 500))
|
| 805 |
+
export_path = Path("/tmp/ranker_logs_export.json")
|
| 806 |
+
_log_adapter.export_json(str(export_path), n=limit)
|
| 807 |
+
return send_file(
|
| 808 |
+
export_path,
|
| 809 |
+
mimetype="application/json",
|
| 810 |
+
as_attachment=True,
|
| 811 |
+
download_name="ranker_logs_export.json",
|
| 812 |
+
)
|
| 813 |
+
except Exception as exc:
|
| 814 |
+
logger.exception(f"[api_logs_export] error: {exc}")
|
| 815 |
+
return jsonify({"error": str(exc)}), 500
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
@app.route("/api/ranker/logs/clear", methods=["POST"])
|
| 819 |
+
def api_logs_clear():
|
| 820 |
+
"""POST /api/ranker/logs/clear β no-op for file-based adapter"""
|
| 821 |
+
try:
|
| 822 |
+
_log_adapter.clear_buffer()
|
| 823 |
+
return jsonify({"status": "cleared"})
|
| 824 |
+
except Exception as exc:
|
| 825 |
+
return jsonify({"error": str(exc)}), 500
|
| 826 |
+
|
| 827 |
+
|
| 828 |
|
| 829 |
|
| 830 |
@app.route("/")
|
|
|
|
| 883 |
|
| 884 |
|
| 885 |
if __name__ == "__main__":
|
| 886 |
+
logger.info("=== K1RL QUASAR HUB DASHBOARD SERVICE v2.4 (SELF-CONTAINED) ===")
|
| 887 |
logger.info(f"Dashboard: http://localhost:{_DASHBOARD_PORT}")
|
| 888 |
logger.info(f"Log directory: {_LOG_DIR}")
|
| 889 |
logger.info("Fixes applied:")
|
| 890 |
+
logger.info(" β
FIX v2.4: All /api/ranker/logs/* routes inline β no Blueprint dependency")
|
| 891 |
+
logger.info(" β
FIX v2.4: Training KPI enrichment (_enrich_training) applied on /recent")
|
| 892 |
logger.info(" β
FIX #1: Now reading *.log* (includes rotated files)")
|
| 893 |
logger.info(" β
FIX #2: Improved regex to catch all trade close formats")
|
| 894 |
logger.info(" β
FIX #4: Dedicated exit_price regex (no optional-group ambiguity)")
|