diff --git a/app/db.py b/app/db.py index 2b7a24f..5f23f9b 100644 --- a/app/db.py +++ b/app/db.py @@ -141,7 +141,8 @@ def list_machines(conn: sqlite3.Connection) -> list[dict]: return [row_to_dict(r) for r in cur.fetchall()] -ONLINE_SECONDS = 180 # 与下载中心一致:3 分钟内心跳视为在线 +# 客户端约 2 秒心跳一次;超过该秒数无心跳视为离线(秒级大屏) +ONLINE_SECONDS = 8 def list_machines_for_user(conn: sqlite3.Connection, user: dict) -> list[dict]: diff --git a/app/routes.py b/app/routes.py index 37047a9..34cfab1 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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") diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index c510d45..6babdb9 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block title %}总览 · {{ app_name }}{% endblock %} {% block content %} -
| 机器 | 状态 | 最近心跳 | {{ m.last_seen_fmt }} | {% else %} -
|---|---|---|
| 当前无在线机器(客户端运行中会每分钟心跳) | ||
| 当前无在线机器(客户端约每 2 秒心跳) | ||