From 679b55f1db41a7582c23879373338927621889de Mon Sep 17 00:00:00 2001 From: dekun Date: Sat, 17 Oct 2026 20:35:16 +0800 Subject: [PATCH] Dashboard: show online machines and live trade feed with polling. Co-authored-by: Cursor --- app/db.py | 33 +++++++++ app/routes.py | 82 ++++++++++++++++++---- app/templates/dashboard.html | 127 ++++++++++++++++++++++++++++++----- 3 files changed, 214 insertions(+), 28 deletions(-) diff --git a/app/db.py b/app/db.py index 00191fa..0ccce36 100644 --- a/app/db.py +++ b/app/db.py @@ -141,6 +141,39 @@ def list_machines(conn: sqlite3.Connection) -> list[dict]: return [row_to_dict(r) for r in cur.fetchall()] +ONLINE_SECONDS = 180 # 与下载中心一致:3 分钟内心跳视为在线 + + +def list_machines_for_user(conn: sqlite3.Connection, user: dict) -> list[dict]: + if user.get("role") == "admin": + items = list_machines(conn) + else: + cur = conn.execute( + """ + SELECT DISTINCT m.* FROM machines m + JOIN activations act ON act.machine_id = m.id + WHERE act.account_id=? + ORDER BY m.id DESC + """, + (user.get("account_id"),), + ) + items = [row_to_dict(r) for r in cur.fetchall()] + return annotate_machines_online(items) + + +def annotate_machines_online(machines: list[dict], now_ts: Optional[float] = None) -> list[dict]: + ts = now_ts if now_ts is not None else now() + for m in machines: + last = m.get("last_seen_at") + try: + online = bool(last and (ts - float(last)) <= ONLINE_SECONDS) + except (TypeError, ValueError): + online = False + m["online"] = online + m["online_label"] = "在线" if online else "离线" + return machines + + def create_activation( conn: sqlite3.Connection, account_id: int, diff --git a/app/routes.py b/app/routes.py index e71f5cc..4ff9170 100644 --- a/app/routes.py +++ b/app/routes.py @@ -179,28 +179,84 @@ def register_routes(app: Flask) -> None: flash(str(exc), "error") return redirect(url_for("users_page")) - @app.get("/") - @login_required - def dashboard(): - u = current_user() + def _fmt_ts(value) -> str: + if value is None or value == "": + return "—" + try: + return datetime.fromtimestamp(float(value)).strftime("%Y-%m-%d %H:%M:%S") + except (TypeError, ValueError, OSError): + return str(value) + + def _dashboard_payload(u: dict) -> dict: with database.get_db() as conn: if u["role"] == "admin": accounts = database.list_accounts(conn) - machines = database.list_machines(conn) - trades = database.list_trades(conn, 20) + trades = database.list_trades(conn, 40) else: accounts = [ a for a in database.list_accounts(conn) if a["id"] == u.get("account_id") ] - machines = database.list_machines(conn) - trades = database.list_trades(conn, 20, account_id=u.get("account_id")) - return render_template( - "dashboard.html", - accounts=accounts, - machines=machines, - trades=trades, + trades = database.list_trades(conn, 40, account_id=u.get("account_id")) + machines = database.list_machines_for_user(conn, u) + online_machines = [m for m in machines if m.get("online")] + for m in machines: + m["last_seen_fmt"] = _fmt_ts(m.get("last_seen_at")) + for t in trades: + t["time_fmt"] = _fmt_ts( + t.get("close_time") or t.get("open_time") or t.get("created_at") + ) + return { + "accounts": accounts, + "machines": machines, + "online_machines": online_machines, + "online_count": len(online_machines), + "machine_count": len(machines), + "trades": trades, + } + + @app.get("/") + @login_required + def dashboard(): + data = _dashboard_payload(current_user()) + return render_template("dashboard.html", **data) + + @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"] + ], + } ) # ---------- 账户(仅管理员创建)---------- diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 70eff90..c510d45 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -4,47 +4,144 @@

总览

账户
{{ accounts|length }}
-
机器
{{ machines|length }}
-
最近成交(展示)
{{ trades|length }}
+
+
在线机器
+
{{ online_count }} / {{ machine_count }}
+
+
实时成交(条)
{{ trades|length }}
+
+

在线机器 · 约 5 秒刷新

+ + + + {% for m in online_machines %} + + + + + + {% else %} + + {% endfor %} + +
机器状态最近心跳
{{ m.name }} #{{ m.id }}在线{{ m.last_seen_fmt }}
当前无在线机器(客户端运行中会每分钟心跳)
+

快速入口

-

登录后下载通用辅助包,用激活码绑定机器与账户;成交自动上报,网页复盘四周期 K 线。

+

下载通用辅助包并用激活码绑定;成交自动上报到本页实时记录与交易复盘。

下载中心 交易复盘

-
-
-

服务信息

-

对外地址:{{ public_base }}

-

端口:5800 · 健康检查:/health

+

对外地址:{{ public_base }}

-

最近记录

+

机器实时交易记录 · 自动刷新

- + + + + - + {% for t in trades %} - + + - + + - + {% else %} - + {% endfor %}
时间品种方向盈亏账户入场形式
时间机器品种方向手数盈亏账户单号
{{ t.close_time or t.open_time or t.created_at }}{{ t.time_fmt }}{{ t.machine_name or '—' }} {{ t.symbol }} {{ t.side }}{{ t.profit if t.profit is not none else '—' }}{{ t.volume if t.volume is not none else '—' }} + {{ t.profit if t.profit is not none else '—' }} + {{ t.account_name or '—' }}{{ t.entry_style or '—' }}{{ t.ticket or '—' }} 复盘
暂无成交。激活客户端并上报后显示。
暂无成交。客户端上报后将实时显示。
{% endblock %} +{% block scripts %} + +{% endblock %}