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 @@
总览
-
-
最近成交(展示)
{{ trades|length }}
+
+
在线机器
+
{{ online_count }} / {{ machine_count }}
+
+
实时成交(条)
{{ trades|length }}
+
+
在线机器 · 约 5 秒刷新
+
+ | 机器 | 状态 | 最近心跳 |
+
+ {% for m in online_machines %}
+
+ | {{ m.name }} #{{ m.id }} |
+ 在线 |
+ {{ m.last_seen_fmt }} |
+
+ {% else %}
+ | 当前无在线机器(客户端运行中会每分钟心跳) |
+ {% endfor %}
+
+
+
快速入口
-
登录后下载通用辅助包,用激活码绑定机器与账户;成交自动上报,网页复盘四周期 K 线。
+
下载通用辅助包并用激活码绑定;成交自动上报到本页实时记录与交易复盘。
下载中心
交易复盘
-
-
-
服务信息
-
对外地址:{{ public_base }}
-
端口:5800 · 健康检查:/health
+
对外地址:{{ public_base }}
-
最近记录
+
机器实时交易记录 · 自动刷新
- | 时间 | 品种 | 方向 | 盈亏 | 账户 | 入场形式 | |
+
+ | 时间 | 机器 | 品种 | 方向 | 手数 |
+ 盈亏 | 账户 | 单号 | |
+
-
+
{% for t in trades %}
- | {{ 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 '—' }} |
复盘 |
{% else %}
- | 暂无成交。激活客户端并上报后显示。 |
+ | 暂无成交。客户端上报后将实时显示。 |
{% endfor %}
{% endblock %}
+{% block scripts %}
+
+{% endblock %}