From 5b4e35c58294a22e045af723469b105a3e3fbedc Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 4 Oct 2026 07:44:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=AD=E6=8E=A7=E8=87=AA?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=A7=A6=E5=8F=91=E4=B8=8E=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=9B=E5=AE=9E=E4=BE=8B=E9=A1=B5=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E8=A1=A8=E6=A0=BC=EF=BC=8C=E5=B8=A6=E9=B1=BC=E5=B1=8F?= =?UTF-8?q?=E5=AE=BD=E5=BA=A6=E9=94=81=201800=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 5 +- lib/update_lib.py | 103 ++++++++++++++++++------ static/hub.css | 160 +++++++++++++++++++------------------- static/instances_page.js | 14 ++-- static/settings_update.js | 48 ++++++++++-- templates/base.html | 4 +- templates/instances.html | 155 +++++++++++++++++++----------------- templates/settings.html | 4 +- 8 files changed, 303 insertions(+), 190 deletions(-) diff --git a/app.py b/app.py index 1512080..8766628 100644 --- a/app.py +++ b/app.py @@ -540,7 +540,10 @@ def api_hub_self_update(): """系统设置:中控本机快速更新.""" from lib.update_lib import schedule_pull_and_restart - result = schedule_pull_and_restart(delay_seconds=1.5) + try: + result = schedule_pull_and_restart(delay_seconds=2.0) + except Exception as e: + result = {"ok": False, "msg": f"触发更新异常: {e}", "deferred": False} return jsonify(result), (200 if result.get("ok") else 400) diff --git a/lib/update_lib.py b/lib/update_lib.py index 88a0b39..487b6cc 100644 --- a/lib/update_lib.py +++ b/lib/update_lib.py @@ -13,6 +13,7 @@ from typing import Any _ROOT = Path(__file__).resolve().parent.parent _update_lock = threading.Lock() _update_pending_until = 0.0 +_UPDATE_LOG = _ROOT / "logs" / "hub_update.log" def _repo_root() -> Path: @@ -41,6 +42,15 @@ def _run_git(args: list[str], *, cwd: Path) -> tuple[bool, str]: return proc.returncode == 0, out +def _append_log(line: str) -> None: + try: + _UPDATE_LOG.parent.mkdir(parents=True, exist_ok=True) + with open(_UPDATE_LOG, "a", encoding="utf-8") as f: + f.write(time.strftime("%Y-%m-%d %H:%M:%S") + " " + line + "\n") + except Exception: + pass + + def read_git_version() -> dict[str, Any]: root = _repo_root() out: dict[str, Any] = { @@ -52,6 +62,7 @@ def read_git_version() -> dict[str, Any]: "dirty": False, "msg": "", "online": True, + "platform": sys.platform, } if not (root / ".git").exists() and not (root / ".git").is_file(): out["ok"] = False @@ -73,35 +84,83 @@ def read_git_version() -> dict[str, Any]: return out -def schedule_pull_and_restart(*, delay_seconds: float = 1.5) -> dict[str, Any]: +def _run_pull_and_restart(root: Path, script: Path) -> None: + log_path = _UPDATE_LOG + try: + log_path.parent.mkdir(parents=True, exist_ok=True) + except Exception: + pass + env = os.environ.copy() + env["REPO"] = str(root) + if not (env.get("PM2_APP_NAME") or "").strip(): + env["PM2_APP_NAME"] = "crypto_okx_hub" + try: + with open(log_path, "a", encoding="utf-8") as logf: + logf.write( + time.strftime("%Y-%m-%d %H:%M:%S") + + f" start pull_and_restart REPO={root}\n" + ) + logf.flush() + proc = subprocess.run( + ["bash", str(script)], + cwd=str(root), + env=env, + stdout=logf, + stderr=subprocess.STDOUT, + text=True, + timeout=180, + ) + logf.write( + time.strftime("%Y-%m-%d %H:%M:%S") + + f" finish returncode={proc.returncode}\n" + ) + except Exception as e: + _append_log("pull_and_restart error: " + (str(e) or "unknown")) + + +def schedule_pull_and_restart(*, delay_seconds: float = 2.0) -> dict[str, Any]: if not sys.platform.startswith("linux"): - return {"ok": False, "msg": "仅 Linux 服务器支持更新", "deferred": False} + msg = f"仅 Linux 服务器支持更新(当前 platform={sys.platform})" + _append_log("reject: " + msg) + return {"ok": False, "msg": msg, "deferred": False, "platform": sys.platform} + root = _repo_root() script = root / "deploy" / "pull_and_restart.sh" if not script.is_file(): - return {"ok": False, "msg": f"缺少更新脚本: {script}", "deferred": False} + msg = f"缺少更新脚本: {script}" + _append_log("reject: " + msg) + return {"ok": False, "msg": msg, "deferred": False} + + if not (root / "ecosystem.config.cjs").is_file(): + msg = f"缺少 ecosystem.config.cjs: {root}" + _append_log("reject: " + msg) + return {"ok": False, "msg": msg, "deferred": False} global _update_pending_until now = time.time() with _update_lock: if now < _update_pending_until: - return {"ok": False, "msg": "已有更新任务进行中,请稍后再试", "deferred": False} - _update_pending_until = now + 120.0 + msg = "已有更新任务进行中,请稍后再试" + return {"ok": False, "msg": msg, "deferred": False} + _update_pending_until = now + 180.0 version = read_git_version() + delay = max(1.0, float(delay_seconds)) + + def _worker() -> None: + try: + time.sleep(delay) + _run_pull_and_restart(root, script) + finally: + with _update_lock: + # 保留短暂冷却,避免连点 + pass + try: - delay = max(0.5, float(delay_seconds)) - cmd = ( - f"sleep {delay} && " - f"cd {shlex.quote(str(root))} && " - f"exec bash {shlex.quote(str(script))}" - ) - subprocess.Popen( - ["bash", "-c", cmd], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - start_new_session=True, - cwd=str(root), + t = threading.Thread(target=_worker, name="hub-self-update", daemon=True) + t.start() + _append_log( + f"accepted before={version.get('short_sha') or ''} delay={delay}s root={root}" ) return { "ok": True, @@ -110,12 +169,12 @@ def schedule_pull_and_restart(*, delay_seconds: float = 1.5) -> dict[str, Any]: "before_sha": version.get("sha") or "", "before_short_sha": version.get("short_sha") or "", "branch": version.get("branch") or "", + "repo": str(root), + "log": str(_UPDATE_LOG), } - except FileNotFoundError: - with _update_lock: - _update_pending_until = 0.0 - return {"ok": False, "msg": "未找到 bash", "deferred": False} except Exception as e: with _update_lock: _update_pending_until = 0.0 - return {"ok": False, "msg": str(e) or "触发更新失败", "deferred": False} + msg = str(e) or "触发更新失败" + _append_log("reject: " + msg) + return {"ok": False, "msg": msg, "deferred": False} diff --git a/static/hub.css b/static/hub.css index 6d91006..57156b0 100644 --- a/static/hub.css +++ b/static/hub.css @@ -83,6 +83,20 @@ body.page-desk .main { margin: 0; padding: 10px 12px 14px; } +/* 实例页:普通屏用满;带鱼屏锁 1800 */ +body.page-instances .main { + max-width: none; + width: 100%; + margin: 0 auto; + padding: 12px clamp(12px, 2vw, 28px) 20px; +} +@media (min-aspect-ratio: 21/9) { + body.page-instances .main { + max-width: 1800px; + padding-left: clamp(16px, 3vw, 48px); + padding-right: clamp(16px, 3vw, 48px); + } +} body.page-login { min-height: 100vh; } @@ -373,7 +387,8 @@ button, .btn { height: 0; } .inst-shell { - max-width: 920px; + max-width: none; + width: 100%; } .inst-add-panel { display: none; @@ -385,73 +400,83 @@ button, .btn { .inst-list-head { border-bottom: 1px solid rgba(56, 189, 248, 0.12); } -.inst-card-list { - padding: 16px 18px 20px; - display: flex; - flex-direction: column; - gap: 12px; +.inst-table-wrap { + padding: 14px 16px 18px; + overflow-x: auto; } .inst-empty { margin: 8px 4px 12px; line-height: 1.5; font-size: 0.88rem; } -.inst-card { - border: 1px solid rgba(56, 189, 248, 0.16); - border-radius: 14px; - background: rgba(5, 12, 22, 0.45); +.inst-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; +} +.inst-table th, +.inst-table td { + padding: 12px 10px; + border-bottom: 1px solid rgba(56, 189, 248, 0.12); + text-align: left; + vertical-align: middle; +} +.inst-table th { + color: var(--accent-2); + font-weight: 600; + white-space: nowrap; + background: rgba(8, 16, 30, 0.7); +} +.inst-table tr.inst-row:hover td { + background: rgba(34, 211, 238, 0.04); +} +.inst-td-name { min-width: 7rem; } +.inst-td-url { + max-width: 28rem; overflow: hidden; -} -.inst-card[open] { - border-color: rgba(56, 189, 248, 0.35); - box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.08); -} -.inst-card-summary { - list-style: none; - display: flex; - align-items: center; - gap: 14px; - padding: 14px 16px; - cursor: pointer; - user-select: none; -} -.inst-card-summary::-webkit-details-marker { display: none; } -.inst-card-main { flex: 1; min-width: 0; } -.inst-card-title-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} -.inst-card-name { - font-size: 1rem; - font-weight: 650; - letter-spacing: 0.02em; -} -.inst-card-url { + text-overflow: ellipsis; + white-space: nowrap; font-size: 0.8rem; - word-break: break-all; - line-height: 1.35; } -.inst-card-aside { - display: flex; +.inst-td-tags { + max-width: 10rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.inst-td-ops { + white-space: nowrap; +} +.inst-td-ops .btn, +.inst-td-ops .btn-danger, +.inst-td-ops label.btn { + margin-top: 0; + display: inline-flex; align-items: center; - gap: 10px; - flex: 0 0 auto; + cursor: pointer; } -.inst-card-chevron { - width: 8px; - height: 8px; - border-right: 2px solid var(--muted); - border-bottom: 2px solid var(--muted); - transform: rotate(45deg); - transition: transform 0.15s ease; - margin-top: -4px; +.inst-inline-del { + display: inline; + margin: 0; } -.inst-card[open] .inst-card-chevron { - transform: rotate(225deg); - margin-top: 2px; +.inst-edit-check { + position: absolute; + opacity: 0; + pointer-events: none; + width: 0; + height: 0; +} +.inst-edit-row > td { + padding: 0; + border-bottom: 1px solid rgba(56, 189, 248, 0.16); + background: rgba(5, 12, 22, 0.35); +} +.inst-edit-panel { + display: none; + padding: 4px 8px 12px; +} +.inst-edit-check:checked + .inst-edit-panel { + display: block; } .inst-pill { display: inline-flex; @@ -472,23 +497,6 @@ button, .btn { background: rgba(127, 29, 29, 0.35); border: 1px solid rgba(251, 113, 133, 0.35); } -.inst-pill--tag { - color: #bae6fd; - background: rgba(14, 116, 144, 0.28); - border: 1px solid rgba(56, 189, 248, 0.28); - max-width: 12rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.inst-card-body { - padding: 0 8px 14px; - border-top: 1px solid rgba(56, 189, 248, 0.1); -} -.inst-card-body .inst-form { - padding-top: 14px; - padding-bottom: 8px; -} .inst-card-actions { display: flex; flex-wrap: wrap; @@ -500,9 +508,6 @@ button, .btn { .inst-card-actions .settings-primary { margin-top: 0; } -.inst-delete-form { - padding: 0 12px 4px; -} .inst-token-wrap { display: flex; gap: 8px; @@ -522,8 +527,7 @@ button, .btn { @media (max-width: 720px) { .inst-page-head { align-items: flex-start; } .settings-row-2 { grid-template-columns: 1fr; } - .inst-card-summary { flex-direction: column; align-items: stretch; gap: 8px; } - .inst-card-aside { justify-content: space-between; } + .inst-td-ops { white-space: normal; } } .overview-table-wrap { overflow-x: auto; diff --git a/static/instances_page.js b/static/instances_page.js index ff5c6b0..f9adfa3 100644 --- a/static/instances_page.js +++ b/static/instances_page.js @@ -7,9 +7,9 @@ return branch + short + (v.dirty ? " (dirty)" : ""); } - async function refreshVersion(card) { - const iid = card.getAttribute("data-id"); - const verEl = card.querySelector("[data-inst-version]"); + async function refreshVersion(row) { + const iid = row.getAttribute("data-id"); + const verEl = row.querySelector("[data-inst-version]"); if (!iid || !verEl) return; verEl.textContent = "…"; verEl.className = "update-version muted"; @@ -49,13 +49,13 @@ function boot() { bindTokenToggles(document); - document.querySelectorAll(".inst-card[data-id]").forEach(function (card) { - refreshVersion(card); - const btn = card.querySelector("[data-inst-refresh-ver]"); + document.querySelectorAll("tr.inst-row[data-id]").forEach(function (row) { + refreshVersion(row); + const btn = row.querySelector("[data-inst-refresh-ver]"); if (btn) { btn.addEventListener("click", function (e) { e.preventDefault(); - refreshVersion(card); + refreshVersion(row); }); } }); diff --git a/static/settings_update.js b/static/settings_update.js index 130ee2f..2e7aa28 100644 --- a/static/settings_update.js +++ b/static/settings_update.js @@ -6,14 +6,39 @@ } async function fetchJson(url, opts) { - const r = await fetch(url, Object || {}); - let data = {}; + const base = opts || {}; + const headers = Object.assign({ Accept: "application/json" }, base.headers || {}); try { - data = await r.json(); + const r = await fetch( + url, + Object.assign({}, base, { + credentials: "same-origin", + headers: headers, + }) + ); + const text = await r.text(); + let data = {}; + try { + data = text ? JSON.parse(text) : {}; + } catch (e) { + data = { + ok: false, + msg: "非 JSON 响应 HTTP " + r.status + (text ? ": " + String(text).slice(0, 120) : ""), + }; + } + if (!data || typeof data !== "object") data = {}; + if (data.ok == null && !r.ok) { + data.ok = false; + data.msg = data.msg || "请求失败 HTTP " + r.status; + } + return { httpOk: r.ok, status: r.status, data: data }; } catch (e) { - data = {}; + return { + httpOk: false, + status: 0, + data: { ok: false, msg: (e && e.message) || "网络错误" }, + }; } - return { httpOk: r.ok, status: r.status, data: data }; } function escapeHtml(s) { @@ -71,7 +96,7 @@ } setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending"); } - setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查服务器日志", "err"); + setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查看 logs/hub_update.log", "err"); return null; } @@ -115,7 +140,10 @@ }); const data = res.data || {}; if (!data.ok) { - setStatus(statusEl, data.msg || "更新失败", "err"); + const detail = + data.msg || + (res.status ? "更新失败 HTTP " + res.status : "更新失败(无响应)"); + setStatus(statusEl, detail, "err"); btn.disabled = false; return; } @@ -178,7 +206,11 @@ }); const data = res.data || {}; if (!data.ok) { - setStatus(statusEl, data.msg || "更新失败", "err"); + setStatus( + statusEl, + data.msg || (res.status ? "更新失败 HTTP " + res.status : "更新失败"), + "err" + ); btn.disabled = false; return; } diff --git a/templates/base.html b/templates/base.html index 6774723..71e8af0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,9 +4,9 @@ {% block title %}期权中控{% endblock %} - + - + {% if session.get('logged_in') %}