修复中控自更新触发与错误提示;实例页改为表格,带鱼屏宽度锁 1800。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -540,7 +540,10 @@ def api_hub_self_update():
|
|||||||
"""系统设置:中控本机快速更新."""
|
"""系统设置:中控本机快速更新."""
|
||||||
from lib.update_lib import schedule_pull_and_restart
|
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)
|
return jsonify(result), (200 if result.get("ok") else 400)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+81
-22
@@ -13,6 +13,7 @@ from typing import Any
|
|||||||
_ROOT = Path(__file__).resolve().parent.parent
|
_ROOT = Path(__file__).resolve().parent.parent
|
||||||
_update_lock = threading.Lock()
|
_update_lock = threading.Lock()
|
||||||
_update_pending_until = 0.0
|
_update_pending_until = 0.0
|
||||||
|
_UPDATE_LOG = _ROOT / "logs" / "hub_update.log"
|
||||||
|
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
def _repo_root() -> Path:
|
||||||
@@ -41,6 +42,15 @@ def _run_git(args: list[str], *, cwd: Path) -> tuple[bool, str]:
|
|||||||
return proc.returncode == 0, out
|
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]:
|
def read_git_version() -> dict[str, Any]:
|
||||||
root = _repo_root()
|
root = _repo_root()
|
||||||
out: dict[str, Any] = {
|
out: dict[str, Any] = {
|
||||||
@@ -52,6 +62,7 @@ def read_git_version() -> dict[str, Any]:
|
|||||||
"dirty": False,
|
"dirty": False,
|
||||||
"msg": "",
|
"msg": "",
|
||||||
"online": True,
|
"online": True,
|
||||||
|
"platform": sys.platform,
|
||||||
}
|
}
|
||||||
if not (root / ".git").exists() and not (root / ".git").is_file():
|
if not (root / ".git").exists() and not (root / ".git").is_file():
|
||||||
out["ok"] = False
|
out["ok"] = False
|
||||||
@@ -73,35 +84,83 @@ def read_git_version() -> dict[str, Any]:
|
|||||||
return out
|
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"):
|
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()
|
root = _repo_root()
|
||||||
script = root / "deploy" / "pull_and_restart.sh"
|
script = root / "deploy" / "pull_and_restart.sh"
|
||||||
if not script.is_file():
|
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
|
global _update_pending_until
|
||||||
now = time.time()
|
now = time.time()
|
||||||
with _update_lock:
|
with _update_lock:
|
||||||
if now < _update_pending_until:
|
if now < _update_pending_until:
|
||||||
return {"ok": False, "msg": "已有更新任务进行中,请稍后再试", "deferred": False}
|
msg = "已有更新任务进行中,请稍后再试"
|
||||||
_update_pending_until = now + 120.0
|
return {"ok": False, "msg": msg, "deferred": False}
|
||||||
|
_update_pending_until = now + 180.0
|
||||||
|
|
||||||
version = read_git_version()
|
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:
|
try:
|
||||||
delay = max(0.5, float(delay_seconds))
|
t = threading.Thread(target=_worker, name="hub-self-update", daemon=True)
|
||||||
cmd = (
|
t.start()
|
||||||
f"sleep {delay} && "
|
_append_log(
|
||||||
f"cd {shlex.quote(str(root))} && "
|
f"accepted before={version.get('short_sha') or ''} delay={delay}s root={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),
|
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"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_sha": version.get("sha") or "",
|
||||||
"before_short_sha": version.get("short_sha") or "",
|
"before_short_sha": version.get("short_sha") or "",
|
||||||
"branch": version.get("branch") 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:
|
except Exception as e:
|
||||||
with _update_lock:
|
with _update_lock:
|
||||||
_update_pending_until = 0.0
|
_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}
|
||||||
|
|||||||
+82
-78
@@ -83,6 +83,20 @@ body.page-desk .main {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px 12px 14px;
|
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 {
|
body.page-login {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -373,7 +387,8 @@ button, .btn {
|
|||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
.inst-shell {
|
.inst-shell {
|
||||||
max-width: 920px;
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.inst-add-panel {
|
.inst-add-panel {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -385,73 +400,83 @@ button, .btn {
|
|||||||
.inst-list-head {
|
.inst-list-head {
|
||||||
border-bottom: 1px solid rgba(56, 189, 248, 0.12);
|
border-bottom: 1px solid rgba(56, 189, 248, 0.12);
|
||||||
}
|
}
|
||||||
.inst-card-list {
|
.inst-table-wrap {
|
||||||
padding: 16px 18px 20px;
|
padding: 14px 16px 18px;
|
||||||
display: flex;
|
overflow-x: auto;
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
.inst-empty {
|
.inst-empty {
|
||||||
margin: 8px 4px 12px;
|
margin: 8px 4px 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
.inst-card {
|
.inst-table {
|
||||||
border: 1px solid rgba(56, 189, 248, 0.16);
|
width: 100%;
|
||||||
border-radius: 14px;
|
border-collapse: collapse;
|
||||||
background: rgba(5, 12, 22, 0.45);
|
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;
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
.inst-card[open] {
|
white-space: nowrap;
|
||||||
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 {
|
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
word-break: break-all;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
}
|
||||||
.inst-card-aside {
|
.inst-td-tags {
|
||||||
display: flex;
|
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;
|
align-items: center;
|
||||||
gap: 10px;
|
cursor: pointer;
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
}
|
||||||
.inst-card-chevron {
|
.inst-inline-del {
|
||||||
width: 8px;
|
display: inline;
|
||||||
height: 8px;
|
margin: 0;
|
||||||
border-right: 2px solid var(--muted);
|
|
||||||
border-bottom: 2px solid var(--muted);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
transition: transform 0.15s ease;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
}
|
||||||
.inst-card[open] .inst-card-chevron {
|
.inst-edit-check {
|
||||||
transform: rotate(225deg);
|
position: absolute;
|
||||||
margin-top: 2px;
|
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 {
|
.inst-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -472,23 +497,6 @@ button, .btn {
|
|||||||
background: rgba(127, 29, 29, 0.35);
|
background: rgba(127, 29, 29, 0.35);
|
||||||
border: 1px solid rgba(251, 113, 133, 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 {
|
.inst-card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -500,9 +508,6 @@ button, .btn {
|
|||||||
.inst-card-actions .settings-primary {
|
.inst-card-actions .settings-primary {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.inst-delete-form {
|
|
||||||
padding: 0 12px 4px;
|
|
||||||
}
|
|
||||||
.inst-token-wrap {
|
.inst-token-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -522,8 +527,7 @@ button, .btn {
|
|||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.inst-page-head { align-items: flex-start; }
|
.inst-page-head { align-items: flex-start; }
|
||||||
.settings-row-2 { grid-template-columns: 1fr; }
|
.settings-row-2 { grid-template-columns: 1fr; }
|
||||||
.inst-card-summary { flex-direction: column; align-items: stretch; gap: 8px; }
|
.inst-td-ops { white-space: normal; }
|
||||||
.inst-card-aside { justify-content: space-between; }
|
|
||||||
}
|
}
|
||||||
.overview-table-wrap {
|
.overview-table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
return branch + short + (v.dirty ? " (dirty)" : "");
|
return branch + short + (v.dirty ? " (dirty)" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshVersion(card) {
|
async function refreshVersion(row) {
|
||||||
const iid = card.getAttribute("data-id");
|
const iid = row.getAttribute("data-id");
|
||||||
const verEl = card.querySelector("[data-inst-version]");
|
const verEl = row.querySelector("[data-inst-version]");
|
||||||
if (!iid || !verEl) return;
|
if (!iid || !verEl) return;
|
||||||
verEl.textContent = "…";
|
verEl.textContent = "…";
|
||||||
verEl.className = "update-version muted";
|
verEl.className = "update-version muted";
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
|
|
||||||
function boot() {
|
function boot() {
|
||||||
bindTokenToggles(document);
|
bindTokenToggles(document);
|
||||||
document.querySelectorAll(".inst-card[data-id]").forEach(function (card) {
|
document.querySelectorAll("tr.inst-row[data-id]").forEach(function (row) {
|
||||||
refreshVersion(card);
|
refreshVersion(row);
|
||||||
const btn = card.querySelector("[data-inst-refresh-ver]");
|
const btn = row.querySelector("[data-inst-refresh-ver]");
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.addEventListener("click", function (e) {
|
btn.addEventListener("click", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
refreshVersion(card);
|
refreshVersion(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,39 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url, opts) {
|
async function fetchJson(url, opts) {
|
||||||
const r = await fetch(url, Object || {});
|
const base = opts || {};
|
||||||
let data = {};
|
const headers = Object.assign({ Accept: "application/json" }, base.headers || {});
|
||||||
try {
|
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) {
|
} 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) {
|
function escapeHtml(s) {
|
||||||
@@ -71,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||||
}
|
}
|
||||||
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查服务器日志", "err");
|
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查看 logs/hub_update.log", "err");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +140,10 @@
|
|||||||
});
|
});
|
||||||
const data = res.data || {};
|
const data = res.data || {};
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
setStatus(statusEl, data.msg || "更新失败", "err");
|
const detail =
|
||||||
|
data.msg ||
|
||||||
|
(res.status ? "更新失败 HTTP " + res.status : "更新失败(无响应)");
|
||||||
|
setStatus(statusEl, detail, "err");
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -178,7 +206,11 @@
|
|||||||
});
|
});
|
||||||
const data = res.data || {};
|
const data = res.data || {};
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
setStatus(statusEl, data.msg || "更新失败", "err");
|
setStatus(
|
||||||
|
statusEl,
|
||||||
|
data.msg || (res.status ? "更新失败 HTTP " + res.status : "更新失败"),
|
||||||
|
"err"
|
||||||
|
);
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}期权中控{% endblock %}</title>
|
<title>{% block title %}期权中控{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=12">
|
<link rel="stylesheet" href="{{ url_for('static', filename='hub.css') }}?v=13">
|
||||||
</head>
|
</head>
|
||||||
<body class="{% if request.path.startswith('/login') %}page-login{% elif request.path.startswith('/desk') %}page-desk{% elif request.path.startswith('/stats') %}page-stats{% elif request.path == '/' %}page-dashboard{% endif %}">
|
<body class="{% if request.path.startswith('/login') %}page-login{% elif request.path.startswith('/desk') %}page-desk{% elif request.path.startswith('/stats') %}page-stats{% elif request.path.startswith('/instances') %}page-instances{% elif request.path == '/' %}page-dashboard{% endif %}">
|
||||||
{% if session.get('logged_in') %}
|
{% if session.get('logged_in') %}
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<nav>
|
<nav>
|
||||||
|
|||||||
+85
-70
@@ -65,87 +65,102 @@
|
|||||||
<div class="settings-card-head inst-list-head">
|
<div class="settings-card-head inst-list-head">
|
||||||
<span class="settings-badge">Registry</span>
|
<span class="settings-badge">Registry</span>
|
||||||
<h2>已登记 · {{ instances|length }}</h2>
|
<h2>已登记 · {{ instances|length }}</h2>
|
||||||
<p class="muted">点击卡片展开编辑;Token 默认隐藏。</p>
|
<p class="muted">表格查看;点「编辑」展开修改。Token 默认隐藏。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inst-card-list">
|
<div class="inst-table-wrap">
|
||||||
{% if not instances %}
|
{% if not instances %}
|
||||||
<p class="muted inst-empty">暂无实例。先在客户端配置 <code>HUB_BRIDGE_TOKEN</code> 并保证公网可访问,再点右上角「添加实例」。</p>
|
<p class="muted inst-empty">暂无实例。先在客户端配置 <code>HUB_BRIDGE_TOKEN</code> 并保证公网可访问,再点右上角「添加实例」。</p>
|
||||||
{% endif %}
|
{% else %}
|
||||||
{% for inst in instances %}
|
<table class="inst-table">
|
||||||
<details class="inst-card" data-id="{{ inst.id }}" data-name="{{ inst.name }}">
|
<thead>
|
||||||
<summary class="inst-card-summary">
|
<tr>
|
||||||
<div class="inst-card-main">
|
<th>名称</th>
|
||||||
<div class="inst-card-title-row">
|
<th>URL</th>
|
||||||
<strong class="inst-card-name">{{ inst.name }}</strong>
|
<th>状态</th>
|
||||||
|
<th>版本</th>
|
||||||
|
<th>标签</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for inst in instances %}
|
||||||
|
<tr class="inst-row" data-id="{{ inst.id }}" data-name="{{ inst.name }}">
|
||||||
|
<td class="inst-td-name"><strong>{{ inst.name }}</strong></td>
|
||||||
|
<td class="inst-td-url muted" title="{{ inst.base_url }}">{{ inst.base_url }}</td>
|
||||||
|
<td>
|
||||||
{% if inst.enabled %}
|
{% if inst.enabled %}
|
||||||
<span class="inst-pill inst-pill--on">启用</span>
|
<span class="inst-pill inst-pill--on">启用</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="inst-pill inst-pill--off">停用</span>
|
<span class="inst-pill inst-pill--off">停用</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if inst.tags %}
|
</td>
|
||||||
<span class="inst-pill inst-pill--tag">{{ inst.tags }}</span>
|
<td><span class="update-version muted" data-inst-version>—</span></td>
|
||||||
{% endif %}
|
<td class="inst-td-tags muted">{{ inst.tags or '—' }}</td>
|
||||||
</div>
|
<td class="inst-td-ops">
|
||||||
<div class="inst-card-url muted">{{ inst.base_url }}</div>
|
<label class="btn btn-sm" for="inst-edit-{{ inst.id }}">编辑</label>
|
||||||
</div>
|
<a class="btn btn-sm" href="/desk?id={{ inst.id }}">内嵌</a>
|
||||||
<div class="inst-card-aside">
|
<button type="button" class="btn btn-sm btn-secondary" data-inst-refresh-ver>版本</button>
|
||||||
<span class="update-version muted" data-inst-version title="点击卡片展开后可刷新">—</span>
|
<form method="post" class="inst-inline-del" onsubmit="return confirm('确认删除「{{ inst.name }}」?')">
|
||||||
<span class="inst-card-chevron" aria-hidden="true"></span>
|
<input type="hidden" name="action" value="delete">
|
||||||
</div>
|
<input type="hidden" name="id" value="{{ inst.id }}">
|
||||||
</summary>
|
<button type="submit" class="btn-danger btn-sm">删除</button>
|
||||||
<div class="inst-card-body">
|
</form>
|
||||||
<form method="post" class="settings-form inst-form" autocomplete="off">
|
</td>
|
||||||
<input type="hidden" name="action" value="update">
|
</tr>
|
||||||
<input type="hidden" name="id" value="{{ inst.id }}">
|
<tr class="inst-edit-row">
|
||||||
<div class="settings-row-2">
|
<td colspan="6">
|
||||||
<label class="settings-field">名称
|
<input type="checkbox" id="inst-edit-{{ inst.id }}" class="inst-edit-check">
|
||||||
<input name="name" value="{{ inst.name }}" required autocomplete="off">
|
<div class="inst-edit-panel">
|
||||||
</label>
|
<form method="post" class="settings-form inst-form" autocomplete="off">
|
||||||
<label class="settings-field">标签
|
<input type="hidden" name="action" value="update">
|
||||||
<input name="tags" value="{{ inst.tags or '' }}" autocomplete="off">
|
<input type="hidden" name="id" value="{{ inst.id }}">
|
||||||
</label>
|
<div class="settings-row-2">
|
||||||
</div>
|
<label class="settings-field">名称
|
||||||
<label class="settings-field">公网 URL
|
<input name="name" value="{{ inst.name }}" required autocomplete="off">
|
||||||
<input name="base_url" value="{{ inst.base_url }}" required autocomplete="off">
|
</label>
|
||||||
</label>
|
<label class="settings-field">标签
|
||||||
<div class="settings-row-2">
|
<input name="tags" value="{{ inst.tags or '' }}" autocomplete="off">
|
||||||
<label class="settings-field">Bridge Token
|
</label>
|
||||||
<span class="inst-token-wrap">
|
</div>
|
||||||
<input type="password" name="bridge_token" value="{{ inst.bridge_token }}" required autocomplete="new-password" class="inst-token-input">
|
<label class="settings-field">公网 URL
|
||||||
<button type="button" class="btn btn-sm btn-secondary inst-token-toggle" aria-label="显示 Token">显示</button>
|
<input name="base_url" value="{{ inst.base_url }}" required autocomplete="off">
|
||||||
</span>
|
</label>
|
||||||
</label>
|
<div class="settings-row-2">
|
||||||
<label class="settings-field">Embed 路径
|
<label class="settings-field">Bridge Token
|
||||||
<input name="embed_path" value="{{ inst.embed_path }}" autocomplete="off">
|
<span class="inst-token-wrap">
|
||||||
</label>
|
<input type="password" name="bridge_token" value="{{ inst.bridge_token }}" required autocomplete="new-password" class="inst-token-input">
|
||||||
</div>
|
<button type="button" class="btn btn-sm btn-secondary inst-token-toggle" aria-label="显示 Token">显示</button>
|
||||||
<label class="settings-switch">
|
</span>
|
||||||
<input type="checkbox" name="enabled" value="1" {% if inst.enabled %}checked{% endif %}>
|
</label>
|
||||||
<span class="settings-switch-ui" aria-hidden="true"></span>
|
<label class="settings-field">Embed 路径
|
||||||
<span class="settings-switch-text">
|
<input name="embed_path" value="{{ inst.embed_path }}" autocomplete="off">
|
||||||
<strong>启用拉取</strong>
|
</label>
|
||||||
<small>关闭后总览不再巡检此机</small>
|
</div>
|
||||||
</span>
|
<label class="settings-switch">
|
||||||
</label>
|
<input type="checkbox" name="enabled" value="1" {% if inst.enabled %}checked{% endif %}>
|
||||||
<div class="inst-card-actions">
|
<span class="settings-switch-ui" aria-hidden="true"></span>
|
||||||
<button type="submit" class="settings-primary">保存</button>
|
<span class="settings-switch-text">
|
||||||
<a class="btn" href="/desk?id={{ inst.id }}">内嵌打开</a>
|
<strong>启用拉取</strong>
|
||||||
<a class="btn btn-secondary" href="/settings?tab=update">系统更新</a>
|
<small>关闭后总览不再巡检此机</small>
|
||||||
<button type="button" class="btn btn-secondary btn-sm" data-inst-refresh-ver>刷新版本</button>
|
</span>
|
||||||
</div>
|
</label>
|
||||||
</form>
|
<div class="inst-card-actions">
|
||||||
<form method="post" class="inst-delete-form" onsubmit="return confirm('确认删除「{{ inst.name }}」?')">
|
<button type="submit" class="settings-primary">保存</button>
|
||||||
<input type="hidden" name="action" value="delete">
|
<label for="inst-edit-{{ inst.id }}" class="btn btn-secondary">收起</label>
|
||||||
<input type="hidden" name="id" value="{{ inst.id }}">
|
<a class="btn btn-secondary" href="/settings?tab=update">系统更新</a>
|
||||||
<button type="submit" class="btn-danger btn-sm">删除实例</button>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='instances_page.js') }}?v=1"></script>
|
<script src="{{ url_for('static', filename='instances_page.js') }}?v=2"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@
|
|||||||
<div class="settings-card-head">
|
<div class="settings-card-head">
|
||||||
<span class="settings-badge">Update</span>
|
<span class="settings-badge">Update</span>
|
||||||
<h2>系统更新</h2>
|
<h2>系统更新</h2>
|
||||||
<p class="muted">快速更新 = git pull + pm2 restart。中控本机更新;客户端经 Bridge 远程下发。有持仓时慎用。</p>
|
<p class="muted">快速更新 = git pull + pm2 restart。中控本机更新;客户端经 Bridge 远程下发。有持仓时慎用。中控更新日志:<code>logs/hub_update.log</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-backup-body">
|
<div class="settings-backup-body">
|
||||||
@@ -183,7 +183,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="{{ url_for('static', filename='settings_backup.js') }}?v=2"></script>
|
<script src="{{ url_for('static', filename='settings_backup.js') }}?v=2"></script>
|
||||||
<script src="{{ url_for('static', filename='settings_update.js') }}?v=1"></script>
|
<script src="{{ url_for('static', filename='settings_update.js') }}?v=2"></script>
|
||||||
<script>
|
<script>
|
||||||
HubSettingsBackup.boot();
|
HubSettingsBackup.boot();
|
||||||
HubSettingsUpdate.boot();
|
HubSettingsUpdate.boot();
|
||||||
|
|||||||
Reference in New Issue
Block a user