diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py
index ea81721..faa9a1c 100644
--- a/crypto_monitor_binance/app.py
+++ b/crypto_monitor_binance/app.py
@@ -6828,7 +6828,7 @@ def fetch_binance_net_pnl_for_trade(
# ====================== 主页面 ======================
-def render_main_page(page="trade"):
+def render_main_page(page="trade", embed_mode=None):
now = app_now()
trading_day = get_trading_day(now)
list_window = _list_window_from_request()
@@ -6909,8 +6909,9 @@ def render_main_page(page="trade"):
if not order_list and exchange_private_api_configured():
orphan_live_positions = list_orphan_live_positions(conn)
conn.close()
- return render_template(
- "index.html",
+ from instance_embed_lib import embed_context_extras
+
+ template_ctx = dict(
page=page,
key=key_list,
key_history=key_history,
@@ -6979,7 +6980,13 @@ def render_main_page(page="trade"):
key_rule_ctx=key_rule_ctx,
kline_timeframe=KLINE_TIMEFRAME,
**strategy_extra,
+ **embed_context_extras("binance"),
)
+ if embed_mode == "fragment":
+ return render_template("embed_page_fragment.html", **template_ctx)
+ if embed_mode == "shell":
+ return render_template("embed_shell.html", initial_tab=page, **template_ctx)
+ return render_template("index.html", **template_ctx)
@app.route("/")
@@ -9375,6 +9382,8 @@ try:
market_fn=_hub_fetch_market,
risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close,
+ render_main_page_fn=render_main_page,
+ login_required_fn=login_required,
)
except Exception as _hub_err:
print(f"[hub_bridge] binance: {_hub_err}")
diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py
index d16500f..6e8567f 100644
--- a/crypto_monitor_gate/app.py
+++ b/crypto_monitor_gate/app.py
@@ -6702,7 +6702,7 @@ def sync_trade_records_from_exchange(conn, force=False):
# ====================== 主页面 ======================
-def render_main_page(page="trade"):
+def render_main_page(page="trade", embed_mode=None):
now = app_now()
trading_day = get_trading_day(now)
list_window = _list_window_from_request()
@@ -6726,7 +6726,10 @@ def render_main_page(page="trade"):
for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
exchange_pnl_sync = {}
- if exchange_private_api_configured() and not request_is_hub_soft_nav():
+ if exchange_private_api_configured() and not request_is_hub_soft_nav() and embed_mode not in (
+ "fragment",
+ "shell",
+ ):
try:
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
except Exception as e:
@@ -6786,8 +6789,9 @@ def render_main_page(page="trade"):
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
conn.close()
- return render_template(
- "index.html",
+ from instance_embed_lib import embed_context_extras
+
+ template_ctx = dict(
page=page,
key=key_list,
key_history=key_history,
@@ -6859,7 +6863,17 @@ def render_main_page(page="trade"):
kline_timeframe=KLINE_TIMEFRAME,
exchange_pnl_sync=exchange_pnl_sync,
**strategy_extra,
+ **embed_context_extras("gate"),
)
+ if embed_mode == "fragment":
+ return render_template("embed_page_fragment.html", **template_ctx)
+ if embed_mode == "shell":
+ return render_template(
+ "embed_shell.html",
+ initial_tab=page,
+ **template_ctx,
+ )
+ return render_template("index.html", **template_ctx)
@app.route("/api/sync_exchange_pnl")
@@ -9321,6 +9335,8 @@ try:
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close,
+ render_main_page_fn=render_main_page,
+ login_required_fn=login_required,
)
except Exception as _hub_err:
print(f"[hub_bridge] gate: {_hub_err}")
diff --git a/crypto_monitor_gate_bot/app.py b/crypto_monitor_gate_bot/app.py
index 500bb15..1ef086d 100644
--- a/crypto_monitor_gate_bot/app.py
+++ b/crypto_monitor_gate_bot/app.py
@@ -6702,7 +6702,7 @@ def sync_trade_records_from_exchange(conn, force=False):
# ====================== 主页面 ======================
-def render_main_page(page="trade"):
+def render_main_page(page="trade", embed_mode=None):
now = app_now()
trading_day = get_trading_day(now)
list_window = _list_window_from_request()
@@ -6726,7 +6726,10 @@ def render_main_page(page="trade"):
for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
exchange_pnl_sync = {}
- if exchange_private_api_configured() and not request_is_hub_soft_nav():
+ if exchange_private_api_configured() and not request_is_hub_soft_nav() and embed_mode not in (
+ "fragment",
+ "shell",
+ ):
try:
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
except Exception as e:
@@ -6786,8 +6789,9 @@ def render_main_page(page="trade"):
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
conn.close()
- return render_template(
- "index.html",
+ from instance_embed_lib import embed_context_extras
+
+ template_ctx = dict(
page=page,
key=key_list,
key_history=key_history,
@@ -6859,7 +6863,13 @@ def render_main_page(page="trade"):
kline_timeframe=KLINE_TIMEFRAME,
exchange_pnl_sync=exchange_pnl_sync,
**strategy_extra,
+ **embed_context_extras("gate_bot"),
)
+ if embed_mode == "fragment":
+ return render_template("embed_page_fragment.html", **template_ctx)
+ if embed_mode == "shell":
+ return render_template("embed_shell.html", initial_tab=page, **template_ctx)
+ return render_template("index.html", **template_ctx)
@app.route("/api/sync_exchange_pnl")
@@ -9321,6 +9331,8 @@ try:
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close,
+ render_main_page_fn=render_main_page,
+ login_required_fn=login_required,
)
except Exception as _hub_err:
print(f"[hub_bridge] gate_bot: {_hub_err}")
diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py
index 9c7219c..a621e56 100644
--- a/crypto_monitor_okx/app.py
+++ b/crypto_monitor_okx/app.py
@@ -6207,7 +6207,7 @@ def api_sync_positions():
# ====================== 主页面 ======================
-def render_main_page(page="trade"):
+def render_main_page(page="trade", embed_mode=None):
now = app_now()
trading_day = get_trading_day(now)
list_window = _list_window_from_request()
@@ -6230,7 +6230,10 @@ def render_main_page(page="trade"):
for o in raw_order_list:
order_list.append(enrich_order_item(row_to_dict(o), current_capital))
exchange_pnl_sync = {}
- if exchange_private_api_configured() and not request_is_hub_soft_nav():
+ if exchange_private_api_configured() and not request_is_hub_soft_nav() and embed_mode not in (
+ "fragment",
+ "shell",
+ ):
try:
exchange_pnl_sync = sync_trade_records_from_exchange(conn) or {}
except Exception as e:
@@ -6292,8 +6295,9 @@ def render_main_page(page="trade"):
trend_cfg=app.extensions.get("strategy_trend_cfg"),
)
conn.close()
- return render_template(
- "index.html",
+ from instance_embed_lib import embed_context_extras
+
+ template_ctx = dict(
page=page,
key=key_list,
key_history=key_history,
@@ -6364,7 +6368,13 @@ def render_main_page(page="trade"):
funding_usdt=funding_usdt,
exchange_pnl_sync=exchange_pnl_sync,
**strategy_extra,
+ **embed_context_extras("okx"),
)
+ if embed_mode == "fragment":
+ return render_template("embed_page_fragment.html", **template_ctx)
+ if embed_mode == "shell":
+ return render_template("embed_shell.html", initial_tab=page, **template_ctx)
+ return render_template("index.html", **template_ctx)
@app.route("/api/sync_exchange_pnl")
@@ -8788,6 +8798,8 @@ try:
market_fn=_hub_fetch_market,
risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close,
+ render_main_page_fn=render_main_page,
+ login_required_fn=login_required,
)
except Exception as _hub_err:
print(f"[hub_bridge] okx: {_hub_err}")
diff --git a/embed_templates/embed_boot_scripts.html b/embed_templates/embed_boot_scripts.html
new file mode 100644
index 0000000..8e83b57
--- /dev/null
+++ b/embed_templates/embed_boot_scripts.html
@@ -0,0 +1,1338 @@
+
diff --git a/embed_templates/embed_page_fragment.html b/embed_templates/embed_page_fragment.html
new file mode 100644
index 0000000..efdea98
--- /dev/null
+++ b/embed_templates/embed_page_fragment.html
@@ -0,0 +1,464 @@
+{# Hub iframe tab fragment — shared via embed_templates #}
+{% macro period_stats(title, s) %}
+
+
{{ title }}
+
{{ s.range_label }}
+
+
+
+
胜率
{% if s.win_rate_pct is not none %}{{ s.win_rate_pct }}%{% else %}-{% endif %}
+
净盈亏(U)
{{ funds_fmt(s.net_pnl_u) }}
+
亏损额合计(U)
{{ funds_fmt(s.loss_sum_u) }}
+
单笔最大亏损(U)
{% if s.max_single_loss is not none %}{{ funds_fmt(s.max_single_loss) }}{% else %}-{% endif %}
+
单笔最大盈利(U)
{% if s.max_single_profit is not none %}{{ funds_fmt(s.max_single_profit) }}{% else %}-{% endif %}
+
最大回撤(U)
{{ funds_fmt(s.max_drawdown_u) }}
+
当前连续亏损笔数
{{ s.consecutive_losses }}
+
最长连续亏损(交易日)
{{ s.max_loss_streak_days }} 天
+
期内最大亏损日
{% if s.worst_day %}{{ s.worst_day }}({{ funds_fmt(s.worst_day_pnl) }}U){% else %}-{% endif %}
+
+
+{% endmacro %}
+
+ {% if page == 'key_monitor' %}
+ {% include 'key_monitor_panel.html' %}
+ {% elif page == 'trade' %}
+
+
+
+
实盘下单监控
+ {% if focus_order_id %}
+
放大查看K线(100根)
+ {% else %}
+
暂无持仓可放大
+ {% endif %}
+
+ {% include order_rule_tips_tpl %}
+
+
+
+
+
+
+
挂止盈止损
+
将先撤销该合约已有 TP/SL,再按下列价格重挂。
+
+
+ 价格模式
+ 百分比模式
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 先撤后挂
+
+
+
+
+
+ {% elif page in ('strategy', 'strategy_trend', 'strategy_roll') %}
+ {% include 'strategy_trading_page.html' %}
+ {% elif page == 'strategy_records' %}
+ {% include 'strategy_records_page.html' %}
+ {% endif %}
+
+
+
+ {% if page == 'records' %}
+
+
交易记录 & 错过机会
+
+
+
+ 修改/核对开关(开启后可编辑关键字段)
+
+
+
+
+ 品种 类型 方向 成交 止损(开仓) 止盈 基数 杠杆 持仓分钟 开仓时间(北京) 平仓时间(北京) 盈亏U 结果 操作
+ {% for r in record %}
+
+ {% set pnl_val = (r.pnl_amount or 0)|float %}
+ {{ r.symbol }}
+ {{ r.monitor_type }}{% if r.key_signal_type %} · {{ r.key_signal_type }}{% endif %}
+ {{ '做多' if r.direction == 'long' else '做空' }}
+ {{ price_fmt(r.symbol, r.trigger_price) }}
+ {% set stop_show = r.display_open_stop_loss or r.initial_stop_loss or r.stop_loss %}
+ {% set tp_show = r.effective_take_profit or r.take_profit %}
+ {{ price_fmt(r.symbol, stop_show) }}
+ {{ price_fmt(r.symbol, tp_show) }}
+ {% if r.margin_capital is not none and r.margin_capital != '' %}{{ funds_fmt(r.margin_capital) }}{% else %}-{% endif %}
+ {{ r.leverage or '-' }}
+ {{ r.effective_hold_minutes or 0 }}
+ {{ (r.effective_opened_at or '-')[:16] }}
+ {{ (r.effective_closed_at or r.created_at or '-')[:16] }}
+ {% set pnl_val = (r.effective_pnl_amount or 0)|float %}
+ {{ funds_fmt(r.effective_pnl_amount or 0) }} {% if r.display_pnl_source == 'exchange' %}所 {% elif r.display_pnl_source != 'reviewed' %}估 {% endif %}
+
+ {% set effective_result = r.effective_result %}
+ {% if effective_result in ["止盈","保本止盈","移动止盈"] %}{{ effective_result }}
+ {% elif effective_result in ["止损","强制清仓","手动平仓"] %}{{ effective_result }}
+ {% elif effective_result == "时间平仓" %}{{ effective_result }}
+ {% else %}{{ effective_result }} {% endif %}
+
+
+ 填入复盘
+ 核对修改
+ 删除
+
+
+ {% endfor %}
+
+
+
+
+
+
记录错过机会
+
+
+
+
+
+
+
+
+ {% endif %}
+
+ {% if page == 'stats' %}
+
+
+
数据统计
+ 折叠
+
+
+
+
持仓占用导致错过(累计)
{{ occupied_miss_total }}
+
+
+ 统计分析按北京时间 {{ stats_bundle.stats_reset_hour }}:00 切日计入(与顶栏 UTC 列表窗无关)。历史总开仓(累计):
+ {{ stats_bundle.total_opens_all }} 次
+
+
+
+ 统计品类
+
+ {% for seg in stats_bundle.segments %}
+ {{ seg.title }}
+ {% endfor %}
+
+
+
+ {% for seg in stats_bundle.segments %}
+
+ {{ period_stats("日统计", seg.day) }}
+ {{ period_stats("周统计", seg.week) }}
+ {{ period_stats("月统计", seg.month) }}
+
+ {% endfor %}
+
+
+ {% endif %}
diff --git a/embed_templates/embed_shell.html b/embed_templates/embed_shell.html
new file mode 100644
index 0000000..c7f700a
--- /dev/null
+++ b/embed_templates/embed_shell.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ exchange_display }} · 加密货币 | 交易监控复盘系统
+
+
+
+
+
+ 关键位监控
+ 实盘下单
+ 策略交易
+ 策略交易记录
+ 交易记录与复盘
+ 统计分析
+
+
+
+
+ 列表筛选(UTC ,默认当日):{{ list_window.label }}
+ 预设
+
+ UTC 当日
+ 近 24 小时
+ 近 7 天
+ 自定义
+
+
+
+ 起(UTC)
+ 止(UTC)
+
+ 应用
+ 统计页仍按北京时间 {{ stats_bundle.stats_reset_hour|default(reset_hour) }}:00 切日
+
+
+
+
交易所
{{ exchange_display }}
+
+
+
+
资金账户(USDT)
{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}
+
+
当日资金(交易账户)
{{ funds_fmt(current_capital) }}U
+
+ {% if include_transfer_block %}
+ {% include 'gate_transfer_block.html' %}
+ {% endif %}
+
+
+ {% include 'embed_page_fragment.html' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% include 'embed_boot_scripts.html' %}
+
+
+
diff --git a/hub_bridge.py b/hub_bridge.py
index 0616c79..07433a2 100644
--- a/hub_bridge.py
+++ b/hub_bridge.py
@@ -59,6 +59,9 @@ def install_instance_theme_static(app) -> None:
"form_submit_guard.js": "application/javascript; charset=utf-8",
"key_monitor_form.js": "application/javascript; charset=utf-8",
"time_close_ui.js": "application/javascript; charset=utf-8",
+ "manual_order_rr_preview.js": "application/javascript; charset=utf-8",
+ "instance_page.css": "text/css; charset=utf-8",
+ "instance_embed.js": "application/javascript; charset=utf-8",
"focus_chart_page.js": "application/javascript; charset=utf-8",
"focus_chart_page.css": "text/css; charset=utf-8",
}
@@ -201,6 +204,19 @@ def _hub_json(view_name: str, path: str, form=None):
return jsonify({"ok": False, "messages": [str(e)]})
+def _embed_login_dest(next_path: str) -> str:
+ """embed=1 时把 /trade 等映射到 /embed?tab=…"""
+ ht = (request.args.get("hub_theme") or "").strip().lower()
+ hub_theme = ht if ht in ("light", "dark") else None
+ if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
+ from instance_embed_lib import rewrite_embed_dest
+
+ return rewrite_embed_dest(next_path, hub_theme=hub_theme)
+ if hub_theme:
+ return _merge_query_into_path(next_path, hub_theme=hub_theme)
+ return next_path
+
+
def install_on_app(
app,
*,
@@ -218,6 +234,8 @@ def install_on_app(
reconcile_hub_flat_fn=None,
risk_status_fn=None,
user_close_fn=None,
+ render_main_page_fn=None,
+ login_required_fn=None,
):
app.config["HUB_CTX"] = {
"exchange": exchange,
@@ -239,6 +257,14 @@ def install_on_app(
configure_hub_embed_session(app)
install_instance_theme_static(app)
register_hub_routes(app)
+ if render_main_page_fn and login_required_fn:
+ import os
+
+ from instance_embed_lib import attach_embed_templates, register_embed_routes
+
+ repo_root = os.path.dirname(os.path.abspath(__file__))
+ attach_embed_templates(app, repo_root)
+ register_embed_routes(app, login_required_fn, render_main_page_fn)
def configure_hub_embed_session(app):
@@ -797,25 +823,22 @@ def register_hub_routes(app):
token = (request.args.get("token") or "").strip()
ok, next_path, err = verify_hub_sso_token(token, ex)
if ok:
+ dest_next = _embed_login_dest(next_path) if request.args.get(
+ "embed", ""
+ ).strip().lower() in ("1", "true", "yes", "on") else next_path
if _sso_wants_embed_auth() and request.is_secure:
- boot = mint_hub_embed_bootstrap(ex, next_path)
+ boot = mint_hub_embed_bootstrap(ex, dest_next)
if boot:
from urllib.parse import urlencode as _ue
- qdict = {"t": boot, "next": next_path, "embed": "1"}
+ qdict = {"t": boot, "next": dest_next, "embed": "1"}
ht0 = (request.args.get("hub_theme") or "").strip().lower()
if ht0 in ("light", "dark"):
qdict["hub_theme"] = ht0
return redirect(f"/hub-embed-auth?{_ue(qdict)}")
session["logged_in"] = True
session.modified = True
- dest = next_path
- if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
- dest = _merge_query_into_path(dest, embed="1")
- ht = (request.args.get("hub_theme") or "").strip().lower()
- if ht in ("light", "dark"):
- dest = _merge_query_into_path(dest, hub_theme=ht)
- return redirect(dest)
+ return redirect(_embed_login_dest(next_path))
hint = err or "校验失败"
flash(
f"中控 SSO 未生效({hint})。"
@@ -839,13 +862,7 @@ def register_hub_routes(app):
if ok:
session["logged_in"] = True
session.modified = True
- dest = next_path
- if request.args.get("embed", "").strip().lower() in ("1", "true", "yes", "on"):
- dest = _merge_query_into_path(dest, embed="1")
- ht = (request.args.get("hub_theme") or "").strip().lower()
- if ht in ("light", "dark"):
- dest = _merge_query_into_path(dest, hub_theme=ht)
- return redirect(dest)
+ return redirect(_embed_login_dest(next_path))
hint = err or "校验失败"
flash(f"iframe 登录未生效({hint})。可点本地导航工具栏「实例免密」重试。")
return redirect("/login")
diff --git a/instance_embed_lib.py b/instance_embed_lib.py
new file mode 100644
index 0000000..66c6174
--- /dev/null
+++ b/instance_embed_lib.py
@@ -0,0 +1,132 @@
+"""中控 iframe:壳常驻 + tab 内容 API(/embed、/api/embed/page/)。"""
+
+from __future__ import annotations
+
+import os
+from typing import Callable
+from urllib.parse import parse_qsl, urlencode, urlsplit
+
+from flask import Flask, Response, jsonify, redirect, request, session
+from jinja2 import ChoiceLoader, FileSystemLoader
+
+EMBED_TABS: tuple[str, ...] = (
+ "key_monitor",
+ "trade",
+ "strategy",
+ "strategy_records",
+ "records",
+ "stats",
+)
+
+PATH_TO_EMBED_TAB: dict[str, str] = {
+ "/": "trade",
+ "/trade": "trade",
+ "/key_monitor": "key_monitor",
+ "/strategy": "strategy",
+ "/strategy/trend": "strategy",
+ "/strategy/roll": "strategy",
+ "/strategy/records": "strategy_records",
+ "/records": "records",
+ "/stats": "stats",
+}
+
+ORDER_RULE_TIPS_BY_EXCHANGE: dict[str, str] = {
+ "gate": "order_monitor_rule_tips_gate.html",
+ "gate_bot": "order_monitor_rule_tips_gate.html",
+ "binance": "order_monitor_rule_tips_binance.html",
+ "okx": "order_monitor_rule_tips_okx.html",
+}
+
+
+def order_rule_tips_template(exchange_key: str) -> str:
+ ex = (exchange_key or "").strip().lower()
+ return ORDER_RULE_TIPS_BY_EXCHANGE.get(ex, "order_monitor_rule_tips_gate.html")
+
+
+def include_transfer_block(exchange_key: str) -> bool:
+ return (exchange_key or "").strip().lower() in ("gate", "gate_bot")
+
+
+def path_to_embed_tab(path: str) -> str | None:
+ p = (path or "/").strip()
+ if not p.startswith("/"):
+ p = "/" + p
+ base = urlsplit(p).path.rstrip("/") or "/"
+ return PATH_TO_EMBED_TAB.get(base)
+
+
+def rewrite_embed_dest(path: str, hub_theme: str | None = None) -> str:
+ """embed=1 打开时:/trade → /embed?tab=trade&embed=1"""
+ split = urlsplit(path or "/")
+ tab = path_to_embed_tab(split.path)
+ q = dict(parse_qsl(split.query, keep_blank_values=True))
+ if tab:
+ q["tab"] = tab
+ q["embed"] = "1"
+ ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
+ if ht in ("light", "dark"):
+ q["hub_theme"] = ht
+ return f"/embed?{urlencode(q)}"
+ q["embed"] = "1"
+ ht = (hub_theme or q.get("hub_theme") or "").strip().lower()
+ if ht in ("light", "dark"):
+ q["hub_theme"] = ht
+ dest = split.path or "/"
+ if split.query:
+ dest += "?" + split.query
+ if "embed=1" not in dest:
+ sep = "&" if "?" in dest else "?"
+ dest += f"{sep}embed=1"
+ if ht in ("light", "dark") and "hub_theme=" not in dest:
+ sep = "&" if "?" in dest else "?"
+ dest += f"{sep}hub_theme={ht}"
+ return dest
+
+
+def attach_embed_templates(app: Flask, repo_root: str) -> None:
+ embed_dir = os.path.join(repo_root, "embed_templates")
+ if not os.path.isdir(embed_dir):
+ return
+ existing = app.jinja_loader
+ loaders = [FileSystemLoader(embed_dir)]
+ if existing is not None:
+ if isinstance(existing, ChoiceLoader):
+ loaders = list(existing.loaders) + loaders
+ else:
+ loaders.insert(0, existing)
+ app.jinja_loader = ChoiceLoader(loaders)
+
+
+def register_embed_routes(
+ app: Flask,
+ login_required: Callable,
+ render_main_page_fn: Callable,
+) -> None:
+ app.config["RENDER_MAIN_PAGE_FN"] = render_main_page_fn
+
+ @login_required
+ @app.route("/embed")
+ def embed_shell_page():
+ tab = (request.args.get("tab") or "trade").strip()
+ if tab not in EMBED_TABS:
+ tab = "trade"
+ session["hub_embed_shell"] = True
+ return render_main_page_fn(tab, embed_mode="shell")
+
+ @login_required
+ @app.route("/api/embed/page/")
+ def api_embed_page(tab: str):
+ tab = (tab or "").strip()
+ if tab not in EMBED_TABS:
+ return jsonify({"ok": False, "msg": "unknown tab"}), 404
+ html = render_main_page_fn(tab, embed_mode="fragment")
+ if isinstance(html, Response):
+ html = html.get_data(as_text=True)
+ return jsonify({"ok": True, "page": tab, "html": html})
+
+
+def embed_context_extras(exchange_key: str) -> dict:
+ return {
+ "order_rule_tips_tpl": order_rule_tips_template(exchange_key),
+ "include_transfer_block": include_transfer_block(exchange_key),
+ }
diff --git a/scripts/build_embed_fragment.py b/scripts/build_embed_fragment.py
new file mode 100644
index 0000000..8baec0e
--- /dev/null
+++ b/scripts/build_embed_fragment.py
@@ -0,0 +1,33 @@
+"""Build embed_page_fragment.html from gate index.html."""
+from __future__ import annotations
+
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+src_lines = (ROOT / "crypto_monitor_gate" / "templates" / "index.html").read_text(
+ encoding="utf-8"
+).splitlines()
+
+# 1-based line numbers from index.html
+macro_body = src_lines[243:262] # {% macro %} … {% endmacro %}
+grid_inner = src_lines[328:736] # inside .grid (exclude outer wrapper)
+stats_block = src_lines[738:772]
+
+out_lines = [
+ "{# Hub iframe tab fragment — shared via embed_templates #}",
+ *macro_body,
+ '',
+ *grid_inner,
+ "
",
+ *stats_block,
+]
+
+out_dir = ROOT / "embed_templates"
+out_dir.mkdir(exist_ok=True)
+text = "\n".join(out_lines) + "\n"
+text = text.replace(
+ "{% include 'order_monitor_rule_tips_gate.html' %}",
+ "{% include order_rule_tips_tpl %}",
+)
+(out_dir / "embed_page_fragment.html").write_text(text, encoding="utf-8")
+print("wrote", out_dir / "embed_page_fragment.html", "lines", len(out_lines))
diff --git a/scripts/extract_instance_page_assets.py b/scripts/extract_instance_page_assets.py
new file mode 100644
index 0000000..7902db6
--- /dev/null
+++ b/scripts/extract_instance_page_assets.py
@@ -0,0 +1,49 @@
+"""One-off: extract instance_page.css / instance_page_boot.js from gate index.html."""
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+src = ROOT / "crypto_monitor_gate" / "templates" / "index.html"
+text = src.read_text(encoding="utf-8")
+
+m = re.search(r"", text, re.S)
+if m:
+ (ROOT / "static" / "instance_page.css").write_text(m.group(1).strip() + "\n", encoding="utf-8")
+
+marker = ''
+if marker in text:
+ part = text.split(marker, 1)[1]
+ m2 = re.search(r"\s*