Add SSE second-level dashboard stream and 2s client heartbeat.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-17 21:04:55 +08:00
parent ba74a9cce8
commit d860156b11
8 changed files with 114 additions and 62 deletions
+64 -34
View File
@@ -3,11 +3,13 @@ from __future__ import annotations
import json
import secrets
import time
from datetime import datetime
from functools import wraps
from flask import (
Flask,
Response,
flash,
g,
jsonify,
@@ -16,6 +18,7 @@ from flask import (
request,
send_from_directory,
session,
stream_with_context,
url_for,
)
@@ -222,42 +225,69 @@ def register_routes(app: Flask) -> None:
data = _dashboard_payload(current_user())
return render_template("dashboard.html", **data)
def _dashboard_live_dict(u: dict) -> dict:
data = _dashboard_payload(u)
return {
"ok": True,
"ts": database.now(),
"online_count": data["online_count"],
"machine_count": data["machine_count"],
"online_machines": [
{
"id": m["id"],
"name": m.get("name") or f"#{m['id']}",
"last_seen_at": m.get("last_seen_at"),
"last_seen_fmt": _fmt_ts(m.get("last_seen_at")),
"online": True,
}
for m in data["online_machines"]
],
"trades": [
{
"id": t["id"],
"time_fmt": t.get("time_fmt"),
"symbol": t.get("symbol"),
"side": t.get("side"),
"volume": t.get("volume"),
"profit": t.get("profit"),
"account_name": t.get("account_name"),
"machine_name": t.get("machine_name"),
"ticket": t.get("ticket"),
}
for t in data["trades"]
],
}
@app.get("/api/dashboard/live")
@login_required
def dashboard_live():
"""总览轮询:在线机器 + 实时成交。"""
data = _dashboard_payload(current_user())
return jsonify(
{
"ok": True,
"online_count": data["online_count"],
"machine_count": data["machine_count"],
"online_machines": [
{
"id": m["id"],
"name": m.get("name") or f"#{m['id']}",
"last_seen_at": m.get("last_seen_at"),
"last_seen_fmt": _fmt_ts(m.get("last_seen_at")),
"online": True,
}
for m in data["online_machines"]
],
"trades": [
{
"id": t["id"],
"time_fmt": t.get("time_fmt"),
"symbol": t.get("symbol"),
"side": t.get("side"),
"volume": t.get("volume"),
"profit": t.get("profit"),
"account_name": t.get("account_name"),
"machine_name": t.get("machine_name"),
"ticket": t.get("ticket"),
}
for t in data["trades"]
],
}
)
"""总览快照(兼容轮询)。"""
return jsonify(_dashboard_live_dict(current_user()))
@app.get("/api/dashboard/stream")
@login_required
def dashboard_stream():
"""秒级 SSE 推送:在线机器 + 实时成交。"""
u = current_user()
@stream_with_context
def event_stream():
# 立即推一帧,后续约每秒一帧
while True:
try:
payload = json.dumps(_dashboard_live_dict(u), ensure_ascii=False)
yield f"data: {payload}\n\n"
except GeneratorExit:
return
except Exception:
yield 'event: error\ndata: {"ok":false}\n\n'
time.sleep(1)
resp = Response(event_stream(), mimetype="text/event-stream")
resp.headers["Cache-Control"] = "no-cache"
resp.headers["X-Accel-Buffering"] = "no"
resp.headers["Connection"] = "keep-alive"
return resp
# ---------- 账户(仅管理员创建)----------
@app.get("/accounts")
@@ -404,7 +434,7 @@ def register_routes(app: Flask) -> None:
).fetchall()
activations = []
now_ts = database.now()
online_sec = 180 # 3 分钟内有心跳视为在线
online_sec = database.ONLINE_SECONDS
for r in acts:
item = database.row_to_dict(r)
last = item.get("machine_last_seen")