Add instance nav prefs, env config UI, and PM2 restart.
P1-P4: configurable nav/section visibility in system settings, full .env editor with restart badges, password change, runtime hot overrides, and single-instance pm2 restart. Works in hub embed iframe. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""实例顶栏 / 系统设置区块显示开关(存 SQLite,即时生效)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from lib.instance.runtime_settings_lib import runtime_get_prefix, runtime_set_many, with_db
|
||||
|
||||
DISPLAY_RUNTIME_PREFIX = "display."
|
||||
|
||||
DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
"show_nav_strategy": True,
|
||||
"show_nav_strategy_records": True,
|
||||
"show_nav_records": True,
|
||||
"show_nav_stats": True,
|
||||
"show_nav_risk_policy": True,
|
||||
"show_nav_env_config": True,
|
||||
"show_nav_options": True,
|
||||
"show_settings_transfer": True,
|
||||
"show_settings_export": True,
|
||||
"show_settings_password": True,
|
||||
"show_settings_options_swap": True,
|
||||
"show_settings_options_transfer": True,
|
||||
}
|
||||
|
||||
DISPLAY_LABELS: dict[str, str] = {
|
||||
"show_nav_strategy": "策略交易",
|
||||
"show_nav_strategy_records": "策略交易记录",
|
||||
"show_nav_records": "交易记录与复盘",
|
||||
"show_nav_stats": "统计分析",
|
||||
"show_nav_risk_policy": "风控说明",
|
||||
"show_nav_env_config": "env配置",
|
||||
"show_nav_options": "期权",
|
||||
"show_settings_transfer": "资金划转",
|
||||
"show_settings_export": "数据导出",
|
||||
"show_settings_password": "账户密码修改",
|
||||
"show_settings_options_swap": "期权币种兑换",
|
||||
"show_settings_options_transfer": "期权资金划转",
|
||||
}
|
||||
|
||||
NAV_TAB_ALLOWED: dict[str, str] = {
|
||||
"strategy": "show_nav_strategy",
|
||||
"strategy_records": "show_nav_strategy_records",
|
||||
"records": "show_nav_records",
|
||||
"stats": "show_nav_stats",
|
||||
"risk_policy": "show_nav_risk_policy",
|
||||
"env_config": "show_nav_env_config",
|
||||
"options": "show_nav_options",
|
||||
}
|
||||
|
||||
|
||||
def normalize_display_prefs(raw: dict | None) -> dict[str, bool]:
|
||||
out = dict(DEFAULT_INSTANCE_DISPLAY)
|
||||
if isinstance(raw, dict):
|
||||
for key in DEFAULT_INSTANCE_DISPLAY:
|
||||
if key in raw:
|
||||
out[key] = bool(raw[key])
|
||||
return out
|
||||
|
||||
|
||||
def _load_from_conn(conn) -> dict[str, bool]:
|
||||
stored = runtime_get_prefix(conn, DISPLAY_RUNTIME_PREFIX)
|
||||
merged: dict[str, Any] = {}
|
||||
for key in DEFAULT_INSTANCE_DISPLAY:
|
||||
sk = key
|
||||
if sk in stored:
|
||||
merged[key] = stored[sk].strip().lower() in ("1", "true", "yes", "on")
|
||||
return normalize_display_prefs(merged)
|
||||
|
||||
|
||||
def get_display_prefs(get_db: Callable) -> dict[str, bool]:
|
||||
return with_db(get_db, _load_from_conn)
|
||||
|
||||
|
||||
def save_display_prefs(get_db: Callable, prefs: dict) -> dict[str, bool]:
|
||||
normalized = normalize_display_prefs(prefs)
|
||||
|
||||
def _save(conn):
|
||||
mapping = {DISPLAY_RUNTIME_PREFIX + k: ("1" if v else "0") for k, v in normalized.items()}
|
||||
runtime_set_many(conn, mapping)
|
||||
return normalized
|
||||
|
||||
return with_db(get_db, _save)
|
||||
|
||||
|
||||
def display_prefs_template_context(get_db: Callable) -> dict[str, Any]:
|
||||
prefs = get_display_prefs(get_db)
|
||||
return {"display": prefs}
|
||||
|
||||
|
||||
def tab_allowed(tab: str, display: Optional[dict[str, bool]] = None) -> bool:
|
||||
prefs = normalize_display_prefs(display or {})
|
||||
key = NAV_TAB_ALLOWED.get((tab or "").strip())
|
||||
if not key:
|
||||
return True
|
||||
return bool(prefs.get(key, True))
|
||||
|
||||
|
||||
def display_meta_for_ui() -> list[dict[str, Any]]:
|
||||
nav_keys = [
|
||||
"show_nav_strategy",
|
||||
"show_nav_strategy_records",
|
||||
"show_nav_records",
|
||||
"show_nav_stats",
|
||||
"show_nav_risk_policy",
|
||||
"show_nav_env_config",
|
||||
"show_nav_options",
|
||||
]
|
||||
settings_keys = [
|
||||
"show_settings_transfer",
|
||||
"show_settings_export",
|
||||
"show_settings_password",
|
||||
"show_settings_options_swap",
|
||||
"show_settings_options_transfer",
|
||||
]
|
||||
return [
|
||||
{"group": "顶栏导航", "keys": [{"key": k, "label": DISPLAY_LABELS[k]} for k in nav_keys]},
|
||||
{"group": "系统设置区块", "keys": [{"key": k, "label": DISPLAY_LABELS[k]} for k in settings_keys]},
|
||||
]
|
||||
@@ -38,7 +38,7 @@ def embed_render_plan(page: str, embed_mode: str | None) -> EmbedRenderPlan:
|
||||
)
|
||||
is_shell = embed_mode == "shell"
|
||||
is_strategy = page in EMBED_STRATEGY_PAGES
|
||||
is_settings_like = page in ("settings", "risk_policy")
|
||||
is_settings_like = page in ("settings", "risk_policy", "env_config")
|
||||
return EmbedRenderPlan(
|
||||
exchange_capitals=is_shell,
|
||||
records_rows=page == "records",
|
||||
|
||||
@@ -19,6 +19,7 @@ EMBED_TABS: tuple[str, ...] = (
|
||||
"records",
|
||||
"stats",
|
||||
"risk_policy",
|
||||
"env_config",
|
||||
"settings",
|
||||
)
|
||||
|
||||
@@ -34,6 +35,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
|
||||
"/records": "records",
|
||||
"/stats": "stats",
|
||||
"/risk_policy": "risk_policy",
|
||||
"/env_config": "env_config",
|
||||
"/settings": "settings",
|
||||
}
|
||||
|
||||
@@ -166,6 +168,9 @@ def register_embed_routes(
|
||||
tab = (tab or "").strip()
|
||||
if tab not in EMBED_TABS:
|
||||
return jsonify({"ok": False, "msg": "unknown tab"}), 404
|
||||
allowed_fn = app.config.get("INSTANCE_TAB_ALLOWED_FN")
|
||||
if callable(allowed_fn) and not allowed_fn(tab):
|
||||
return jsonify({"ok": False, "msg": "tab disabled"}), 403
|
||||
html = render_main_page_fn(tab, embed_mode="fragment")
|
||||
if isinstance(html, Response):
|
||||
html = html.get_data(as_text=True)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""PM2 重启当前实例(仅 Linux 部署环境)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def default_pm2_app_name(exchange_key: str) -> str:
|
||||
mapping = {
|
||||
"okx": "crypto_okx",
|
||||
"binance": "crypto_binance",
|
||||
"gate": "crypto_gate",
|
||||
}
|
||||
return mapping.get((exchange_key or "").strip().lower(), "crypto_okx")
|
||||
|
||||
|
||||
def resolve_pm2_app_name(exchange_key: str) -> str:
|
||||
explicit = (os.getenv("PM2_APP_NAME") or "").strip()
|
||||
if explicit:
|
||||
return explicit
|
||||
return default_pm2_app_name(exchange_key)
|
||||
|
||||
|
||||
def restart_instance_pm2(exchange_key: str) -> dict[str, Any]:
|
||||
if not sys.platform.startswith("linux"):
|
||||
return {"ok": False, "msg": "仅 Linux 服务器支持 PM2 重启", "app": None}
|
||||
app_name = resolve_pm2_app_name(exchange_key)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["pm2", "restart", app_name, "--update-env"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
ok = proc.returncode == 0
|
||||
return {
|
||||
"ok": ok,
|
||||
"app": app_name,
|
||||
"msg": (proc.stdout or proc.stderr or "").strip()[:500],
|
||||
"returncode": proc.returncode,
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {"ok": False, "msg": "未找到 pm2 命令", "app": app_name}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"ok": False, "msg": "pm2 restart 超时", "app": app_name}
|
||||
except Exception as e:
|
||||
return {"ok": False, "msg": str(e), "app": app_name}
|
||||
@@ -184,6 +184,7 @@ def build_instance_settings_view(
|
||||
|
||||
|
||||
def settings_page_context(page: str, **kwargs: Any) -> dict[str, Any]:
|
||||
if (page or "").strip() not in ("settings", "risk_policy"):
|
||||
p = (page or "").strip()
|
||||
if p not in ("settings", "risk_policy", "env_config"):
|
||||
return {}
|
||||
return {"instance_settings": build_instance_settings_view(**kwargs)}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""实例系统设置 API:导航开关、env 读写、改密、PM2 重启。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from flask import jsonify, request, session
|
||||
|
||||
from lib.env.env_file_lib import apply_env_updates, env_get, read_env_lines
|
||||
from lib.env.env_schema import (
|
||||
build_env_payload,
|
||||
parse_env_example_schema,
|
||||
updates_need_restart,
|
||||
validate_env_updates,
|
||||
)
|
||||
from lib.instance.instance_display_prefs_lib import (
|
||||
display_meta_for_ui,
|
||||
get_display_prefs,
|
||||
normalize_display_prefs,
|
||||
save_display_prefs,
|
||||
tab_allowed,
|
||||
)
|
||||
from lib.instance.instance_pm2_lib import restart_instance_pm2
|
||||
from lib.instance.runtime_config_lib import apply_env_reload
|
||||
|
||||
|
||||
def _api_login_required(hub_token_write_allowed: bool = False):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
from lib.hub.hub_auth import request_allowed as hub_request_allowed
|
||||
|
||||
logged_in = bool(session.get("logged_in"))
|
||||
auth_disabled = (os.getenv("APP_AUTH_DISABLED") or "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
hub_hdr = (request.headers.get("X-Hub-Token") or "").strip()
|
||||
bridge = (os.getenv("HUB_BRIDGE_TOKEN") or "").strip()
|
||||
if hub_hdr and bridge and hub_hdr == bridge and not hub_token_write_allowed:
|
||||
return jsonify({"ok": False, "msg": "Hub Token 不可修改实例设置"}), 403
|
||||
if hub_request_allowed(logged_in, auth_disabled):
|
||||
return f(*args, **kwargs)
|
||||
return jsonify({"ok": False, "msg": "未登录"}), 401
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_instance_settings_routes(
|
||||
app,
|
||||
*,
|
||||
get_db: Callable,
|
||||
login_required_fn: Callable,
|
||||
base_dir: str,
|
||||
exchange_key: str,
|
||||
username: str,
|
||||
password: str,
|
||||
) -> None:
|
||||
env_path = os.path.join(base_dir, ".env")
|
||||
example_path = os.path.join(base_dir, ".env.example")
|
||||
api_auth = _api_login_required()
|
||||
|
||||
@app.route("/api/settings/display", methods=["GET"])
|
||||
@api_auth
|
||||
def api_get_display_prefs():
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"display": prefs,
|
||||
"meta": display_meta_for_ui(),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/settings/display", methods=["POST"])
|
||||
@api_auth
|
||||
def api_save_display_prefs():
|
||||
body = request.get_json(silent=True) or {}
|
||||
raw = body.get("display") if isinstance(body.get("display"), dict) else body
|
||||
saved = save_display_prefs(get_db, raw)
|
||||
return jsonify({"ok": True, "display": saved})
|
||||
|
||||
@app.route("/api/settings/env/meta", methods=["GET"])
|
||||
@api_auth
|
||||
def api_env_meta():
|
||||
payload = build_env_payload(example_path, env_path)
|
||||
return jsonify({"ok": True, **payload})
|
||||
|
||||
@app.route("/api/settings/env", methods=["GET"])
|
||||
@api_auth
|
||||
def api_env_get():
|
||||
payload = build_env_payload(example_path, env_path)
|
||||
return jsonify({"ok": True, **payload})
|
||||
|
||||
@app.route("/api/settings/env", methods=["POST"])
|
||||
@api_auth
|
||||
def api_env_post():
|
||||
body = request.get_json(silent=True) or {}
|
||||
updates = body.get("values") if isinstance(body.get("values"), dict) else body
|
||||
if not isinstance(updates, dict):
|
||||
return jsonify({"ok": False, "msg": "无效请求体"}), 400
|
||||
groups = parse_env_example_schema(example_path)
|
||||
clean, errors = validate_env_updates(groups, updates)
|
||||
if errors:
|
||||
return jsonify({"ok": False, "msg": "; ".join(errors)}), 400
|
||||
if not clean:
|
||||
return jsonify({"ok": True, "changed_keys": [], "restart_required": False})
|
||||
changed = apply_env_updates(env_path, clean)
|
||||
reload_info = apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
"changed_keys": changed,
|
||||
"restart_required": reload_info.get("restart_required", False),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/settings/password", methods=["POST"])
|
||||
@api_auth
|
||||
def api_change_password():
|
||||
body = request.get_json(silent=True) or {}
|
||||
old_password = str(body.get("old_password") or "")
|
||||
new_username = str(body.get("new_username") or "").strip()
|
||||
new_password = str(body.get("new_password") or "")
|
||||
confirm = str(body.get("confirm_password") or "")
|
||||
if not old_password or old_password != password:
|
||||
return jsonify({"ok": False, "msg": "当前密码错误"}), 400
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"ok": False, "msg": "新密码至少 6 位"}), 400
|
||||
if new_password != confirm:
|
||||
return jsonify({"ok": False, "msg": "两次输入的新密码不一致"}), 400
|
||||
updates: dict[str, str] = {"APP_PASSWORD": new_password}
|
||||
if new_username:
|
||||
updates["APP_USERNAME"] = new_username
|
||||
changed = apply_env_updates(env_path, updates)
|
||||
groups = parse_env_example_schema(example_path)
|
||||
apply_env_reload(env_path, get_db, changed, groups)
|
||||
return jsonify({"ok": True, "restart_required": True, "changed_keys": changed})
|
||||
|
||||
@app.route("/api/admin/restart", methods=["POST"])
|
||||
@api_auth
|
||||
def api_admin_restart():
|
||||
result = restart_instance_pm2(exchange_key)
|
||||
code = 200 if result.get("ok") else 500
|
||||
return jsonify({"ok": bool(result.get("ok")), **result}), code
|
||||
|
||||
@app.route("/api/admin/health", methods=["GET"])
|
||||
def api_admin_health():
|
||||
return jsonify({"ok": True, "status": "up"})
|
||||
|
||||
def tab_allowed_fn(tab: str) -> bool:
|
||||
prefs = get_display_prefs(get_db)
|
||||
return tab_allowed(tab, prefs)
|
||||
|
||||
app.config["INSTANCE_GET_DB"] = get_db
|
||||
app.config["INSTANCE_TAB_ALLOWED_FN"] = tab_allowed_fn
|
||||
|
||||
@app.route("/api/embed/tab_allowed/<tab>", methods=["GET"])
|
||||
@api_auth
|
||||
def api_tab_allowed(tab: str):
|
||||
prefs = get_display_prefs(get_db)
|
||||
return jsonify({"ok": True, "tab": tab, "allowed": tab_allowed(tab, prefs)})
|
||||
|
||||
|
||||
def merge_ui_template_context(page: str, get_db: Callable, **settings_kwargs: Any) -> dict[str, Any]:
|
||||
from lib.instance.instance_settings_lib import settings_page_context
|
||||
|
||||
prefs = get_display_prefs(get_db)
|
||||
ctx = {
|
||||
"display": prefs,
|
||||
**settings_page_context(page, **settings_kwargs),
|
||||
}
|
||||
return ctx
|
||||
@@ -0,0 +1,62 @@
|
||||
"""env 运行时覆盖:热生效项优先读 SQLite,再回退 os.environ。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Callable, Optional
|
||||
|
||||
from lib.env.env_file_lib import load_env_file_into_environ
|
||||
from lib.instance.runtime_settings_lib import runtime_get, with_db
|
||||
|
||||
ENV_OVERRIDE_PREFIX = "env."
|
||||
|
||||
|
||||
def runtime_env_key(name: str) -> str:
|
||||
return ENV_OVERRIDE_PREFIX + name
|
||||
|
||||
|
||||
def get_config(key: str, get_db: Callable, default: Optional[str] = None) -> Optional[str]:
|
||||
def _read(conn):
|
||||
v = runtime_get(conn, runtime_env_key(key))
|
||||
return v
|
||||
|
||||
try:
|
||||
v = with_db(get_db, _read)
|
||||
if v is not None:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
raw = os.getenv(key)
|
||||
if raw is None or raw == "":
|
||||
return default
|
||||
return raw
|
||||
|
||||
|
||||
def set_config_overrides(get_db: Callable, mapping: dict[str, str]) -> None:
|
||||
from lib.instance.runtime_settings_lib import runtime_set_many
|
||||
|
||||
def _write(conn):
|
||||
payload = {runtime_env_key(k): str(v) for k, v in mapping.items()}
|
||||
runtime_set_many(conn, payload)
|
||||
|
||||
with_db(get_db, _write)
|
||||
|
||||
|
||||
def apply_env_reload(env_path: str, get_db: Callable, changed_keys: list[str], groups: list[dict]) -> dict[str, bool]:
|
||||
"""写盘后同步 os.environ,并将可热生效项写入 runtime 覆盖。"""
|
||||
load_env_file_into_environ(env_path)
|
||||
hot: dict[str, str] = {}
|
||||
field_map = {}
|
||||
for group in groups:
|
||||
for field in group.get("fields") or []:
|
||||
field_map[field["key"]] = field
|
||||
for key in changed_keys:
|
||||
meta = field_map.get(key) or {}
|
||||
if meta.get("hot_reload") and not meta.get("restart_required"):
|
||||
val = os.getenv(key)
|
||||
if val is not None:
|
||||
hot[key] = val
|
||||
if hot:
|
||||
set_config_overrides(get_db, hot)
|
||||
from lib.env.env_schema import updates_need_restart
|
||||
|
||||
return {"restart_required": updates_need_restart(groups, changed_keys)}
|
||||
@@ -0,0 +1,71 @@
|
||||
"""实例 SQLite 运行时配置(导航开关、env 热覆盖等)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
RUNTIME_TABLE_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS app_runtime_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def ensure_runtime_settings_table(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(RUNTIME_TABLE_SQL)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def runtime_get(conn: sqlite3.Connection, key: str) -> Optional[str]:
|
||||
row = conn.execute(
|
||||
"SELECT value FROM app_runtime_settings WHERE key=?",
|
||||
(key,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
val = row["value"] if isinstance(row, sqlite3.Row) else row[0]
|
||||
return None if val is None else str(val)
|
||||
|
||||
|
||||
def runtime_set(conn: sqlite3.Connection, key: str, value: str) -> None:
|
||||
now = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn.execute(
|
||||
"INSERT INTO app_runtime_settings(key, value, updated_at) VALUES (?,?,?) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
|
||||
(key, value, now),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def runtime_get_prefix(conn: sqlite3.Connection, prefix: str) -> dict[str, str]:
|
||||
rows = conn.execute(
|
||||
"SELECT key, value FROM app_runtime_settings WHERE key LIKE ?",
|
||||
(prefix + "%",),
|
||||
).fetchall()
|
||||
out: dict[str, str] = {}
|
||||
for row in rows:
|
||||
k = row["key"] if isinstance(row, sqlite3.Row) else row[0]
|
||||
v = row["value"] if isinstance(row, sqlite3.Row) else row[1]
|
||||
if k.startswith(prefix):
|
||||
out[k[len(prefix) :]] = v if v is not None else ""
|
||||
return out
|
||||
|
||||
|
||||
def runtime_set_many(conn: sqlite3.Connection, mapping: dict[str, str]) -> None:
|
||||
for key, value in mapping.items():
|
||||
runtime_set(conn, key, value)
|
||||
|
||||
|
||||
def with_db(
|
||||
get_db: Callable[[], sqlite3.Connection],
|
||||
fn: Callable[[sqlite3.Connection], Any],
|
||||
) -> Any:
|
||||
conn = get_db()
|
||||
try:
|
||||
ensure_runtime_settings_table(conn)
|
||||
return fn(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -0,0 +1,12 @@
|
||||
{# 系统设置 · 导航显示开关 #}
|
||||
<div class="card settings-card settings-card--compact" id="display-prefs-card">
|
||||
<h2>导航显示</h2>
|
||||
<p class="settings-env-hint">以下开关控制顶栏导航与系统设置内区块是否显示,保存后立即生效。关键位监控、实盘下单、系统设置为固定项。</p>
|
||||
<div id="display-prefs-form" class="display-prefs-form">
|
||||
<div class="display-prefs-loading muted">加载中…</div>
|
||||
</div>
|
||||
<div class="settings-actions-row">
|
||||
<button type="button" class="btn-primary btn-sm" id="display-prefs-save">保存导航设置</button>
|
||||
<span class="settings-status-line" id="display-prefs-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -396,6 +396,9 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if page == 'env_config' %}
|
||||
{% include 'env_config_panel.html' %}
|
||||
{% endif %}
|
||||
{% if page == 'risk_policy' %}
|
||||
{% include 'risk_policy_panel.html' %}
|
||||
{% endif %}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=4">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=70">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=71">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||
@@ -30,22 +30,33 @@
|
||||
<nav class="top-nav embed-top-nav" aria-label="实例导航">
|
||||
<a href="/key_monitor" data-embed-tab="key_monitor" class="{% if initial_tab == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" data-embed-tab="trade" class="{% if initial_tab == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
{% if not intraday_discipline %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" data-embed-tab="strategy" class="{% if initial_tab == 'strategy' %}active{% endif %}">策略交易</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy_records %}
|
||||
<a href="/strategy/records" data-embed-tab="strategy_records" class="{% if initial_tab == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_records %}
|
||||
<a href="/records" data-embed-tab="records" class="{% if initial_tab == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_stats %}
|
||||
<a href="/stats" data-embed-tab="stats" class="{% if initial_tab == 'stats' %}active{% endif %}">统计分析</a>
|
||||
{% if options_nav_visible %}
|
||||
{% endif %}
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" data-embed-tab="options" class="{% if initial_tab == 'options' %}active{% endif %}">期权</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_env_config %}
|
||||
<a href="/env_config" data-embed-tab="env_config" class="{% if initial_tab == 'env_config' %}active{% endif %}">env配置</a>
|
||||
{% endif %}
|
||||
<a href="/settings" data-embed-tab="settings" class="{% if initial_tab == 'settings' %}active{% endif %}">系统设置</a>
|
||||
</nav>
|
||||
<div id="embed-flash" class="flash" style="display:none" role="status"></div>
|
||||
|
||||
{% include 'instance_header_panel.html' %}
|
||||
{% if initial_tab not in ('settings', 'risk_policy') and include_transfer_block %}
|
||||
{% if initial_tab not in ('settings', 'risk_policy', 'env_config') and include_transfer_block %}
|
||||
{% include 'instance_top_bar.html' %}
|
||||
{% endif %}
|
||||
|
||||
@@ -89,7 +100,11 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script src="/static/strategy_roll.js?v=6"></script>
|
||||
<script src="/static/key_monitor_form.js?v=2"></script>
|
||||
{% include 'embed_boot_scripts.html' %}
|
||||
<script src="/static/instance_live.js?v=3"></script>
|
||||
<script src="/static/instance_embed.js?v=16"></script>
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=1"></script>
|
||||
<script src="/static/instance_live.js?v=4"></script>
|
||||
<script src="/static/instance_embed.js?v=17"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{# env配置:按功能分组,三列卡片布局 #}
|
||||
<div class="env-config-page">
|
||||
<div class="env-config-head card">
|
||||
<h2>env 配置</h2>
|
||||
<p class="settings-env-hint">读取并编辑本实例 <code>.env</code>。标注「保存即生效」的项会立即应用;标注「需重启」的项保存后请点「保存并重启」。</p>
|
||||
<div class="env-config-toolbar">
|
||||
<button type="button" class="btn-primary btn-sm" id="env-config-save">保存</button>
|
||||
<button type="button" class="btn-secondary btn-sm" id="env-config-save-restart">保存并重启</button>
|
||||
<button type="button" class="btn-secondary btn-sm" id="env-config-reload">重新加载</button>
|
||||
<span class="settings-status-line" id="env-config-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="env-config-grid" class="env-config-grid">
|
||||
<div class="env-config-loading muted">加载配置中…</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -17,7 +17,7 @@
|
||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=1">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=70">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=71">
|
||||
|
||||
</head>
|
||||
<body
|
||||
@@ -55,22 +55,33 @@
|
||||
<div class="top-nav">
|
||||
<a href="/key_monitor" class="{% if page == 'key_monitor' %}active{% endif %}">关键位监控</a>
|
||||
<a href="/trade" class="{% if page == 'trade' %}active{% endif %}">实盘下单</a>
|
||||
{% if not intraday_discipline %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" class="{% if page in ('strategy', 'strategy_trend', 'strategy_roll') %}active{% endif %}">策略交易</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy_records %}
|
||||
<a href="/strategy/records" class="{% if page == 'strategy_records' %}active{% endif %}">策略交易记录</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_records %}
|
||||
<a href="/records" class="{% if page == 'records' %}active{% endif %}">交易记录与复盘</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_stats %}
|
||||
<a href="/stats" class="{% if page == 'stats' %}active{% endif %}">统计分析</a>
|
||||
{% if options_nav_visible %}
|
||||
{% endif %}
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" class="{% if page == 'options' %}active{% endif %}">期权</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_env_config %}
|
||||
<a href="/env_config" class="{% if page == 'env_config' %}active{% endif %}">env配置</a>
|
||||
{% endif %}
|
||||
<a href="/settings" class="{% if page == 'settings' %}active{% endif %}">系统设置</a>
|
||||
</div>
|
||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||
|
||||
{% include 'instance_header_panel.html' %}
|
||||
{% if page not in ('settings', 'risk_policy', 'options') %}
|
||||
{% if page not in ('settings', 'risk_policy', 'env_config', 'options') %}
|
||||
{% include 'instance_top_bar.html' %}
|
||||
{% endif %}
|
||||
|
||||
@@ -468,6 +479,10 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if page == 'env_config' %}
|
||||
{% include 'env_config_panel.html' %}
|
||||
{% endif %}
|
||||
|
||||
{% if page == 'risk_policy' %}
|
||||
{% include 'risk_policy_panel.html' %}
|
||||
{% endif %}
|
||||
@@ -2020,5 +2035,9 @@ setInterval(tickOrderHoldDurations, 1000);
|
||||
tickOrderHoldDurations();
|
||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||
</script>
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
{# 系统设置 · 账户密码 #}
|
||||
<div class="card settings-subcard settings-subcard--ops" id="password-settings-card" data-settings-section="password">
|
||||
<h3 class="settings-subcard-title">账户密码修改</h3>
|
||||
<p class="settings-subcard-desc">修改网页登录账号密码,写入 <code>.env</code> 后需重启实例生效。</p>
|
||||
<div class="settings-password-form">
|
||||
<label>当前密码 <input type="password" id="pwd-old" autocomplete="current-password"></label>
|
||||
<label>新用户名(可选) <input type="text" id="pwd-new-username" autocomplete="username"></label>
|
||||
<label>新密码 <input type="password" id="pwd-new" autocomplete="new-password"></label>
|
||||
<label>确认新密码 <input type="password" id="pwd-confirm" autocomplete="new-password"></label>
|
||||
</div>
|
||||
<div class="settings-actions-row">
|
||||
<button type="button" class="btn-primary btn-sm" id="pwd-save-btn">保存密码</button>
|
||||
<span class="settings-status-line" id="pwd-save-status"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,19 +1,29 @@
|
||||
{# 系统设置:资金划转 + 数据导出(顶栏资金与统计见 instance_header_panel) #}
|
||||
{# 系统设置:导航显示 + 账户密码 + 资金划转 + 数据导出 #}
|
||||
<div class="settings-page settings-page--ops">
|
||||
{% include 'display_prefs_panel.html' %}
|
||||
|
||||
{% if display.show_settings_password %}
|
||||
{% include 'password_settings_panel.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card settings-card settings-card--side-panel">
|
||||
<div class="settings-side-subcards">
|
||||
{% if instance_settings.options_settings_enabled %}
|
||||
{% if instance_settings.options_settings_enabled and display.show_settings_options_swap %}
|
||||
<div class="card settings-subcard settings-subcard--ops">
|
||||
<h3 class="settings-subcard-title">币种兑换</h3>
|
||||
{% include 'options_settings_swap.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance_settings.options_settings_enabled and display.show_settings_options_transfer %}
|
||||
<div class="card settings-subcard settings-subcard--ops">
|
||||
<h3 class="settings-subcard-title">期权资金划转</h3>
|
||||
{% include 'options_settings_transfer.html' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if instance_settings.options_settings_enabled %}
|
||||
{% include 'options_settings_panel.html' %}
|
||||
{% endif %}
|
||||
{% if instance_settings.show_transfer %}
|
||||
{% if instance_settings.show_transfer and display.show_settings_transfer %}
|
||||
<div class="card settings-subcard settings-subcard--ops">
|
||||
<h3 class="settings-subcard-title">永续资金划转</h3>
|
||||
<p class="settings-subcard-desc">子账户永续:资金账户与交易账户之间划转 USDT。</p>
|
||||
@@ -21,6 +31,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if display.show_settings_export %}
|
||||
<div class="settings-side-export">
|
||||
<div class="settings-side-export-head">
|
||||
<span class="settings-side-export-label">数据导出</span>
|
||||
@@ -33,6 +44,7 @@
|
||||
<a href="/export/key_monitor_history">关键位历史</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user