系统设置增加「系统更新」Tab:中控本机更新与客户端远程更新,实例页入口移除。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+14
-27
@@ -330,36 +330,21 @@ button, .btn {
|
||||
}
|
||||
.check { display: flex !important; align-items: center; gap: 8px; }
|
||||
.inst-row { margin: 10px 0; border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; }
|
||||
.inst-row > summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.inst-version {
|
||||
font-size: 0.78rem;
|
||||
.update-version {
|
||||
font-size: 0.82rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-left: auto;
|
||||
}
|
||||
.inst-version.online { color: var(--ok); }
|
||||
.inst-version.offline { color: var(--bad); }
|
||||
.inst-remote-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
.inst-remote-actions .btn { margin-top: 0; }
|
||||
.inst-update-hint { margin: 0 0 12px; font-size: 0.78rem; }
|
||||
.inst-update-status {
|
||||
.update-version.online { color: var(--ok); }
|
||||
.update-version.offline { color: var(--bad); }
|
||||
.update-status {
|
||||
font-size: 0.8rem;
|
||||
min-height: 1.2em;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.inst-update-status.pending { color: var(--accent-2); }
|
||||
.inst-update-status.ok { color: var(--ok); }
|
||||
.inst-update-status.err { color: var(--bad); }
|
||||
.update-status.pending { color: var(--accent-2); }
|
||||
.update-status.ok { color: var(--ok); }
|
||||
.update-status.err { color: var(--bad); }
|
||||
.update-table td { vertical-align: middle; }
|
||||
.overview-table-wrap {
|
||||
overflow-x: auto;
|
||||
margin-top: 12px;
|
||||
@@ -667,7 +652,8 @@ button, .btn {
|
||||
}
|
||||
#hub-set-account:checked ~ .settings-tabs label[for="hub-set-account"],
|
||||
#hub-set-wechat:checked ~ .settings-tabs label[for="hub-set-wechat"],
|
||||
#hub-set-backup:checked ~ .settings-tabs label[for="hub-set-backup"] {
|
||||
#hub-set-backup:checked ~ .settings-tabs label[for="hub-set-backup"],
|
||||
#hub-set-update:checked ~ .settings-tabs label[for="hub-set-update"] {
|
||||
color: var(--text);
|
||||
background: rgba(34, 211, 238, 0.12);
|
||||
border-color: rgba(56, 189, 248, 0.35);
|
||||
@@ -681,7 +667,8 @@ button, .btn {
|
||||
}
|
||||
#hub-set-account:checked ~ .settings-panels .settings-panel--account,
|
||||
#hub-set-wechat:checked ~ .settings-panels .settings-panel--wechat,
|
||||
#hub-set-backup:checked ~ .settings-panels .settings-panel--backup {
|
||||
#hub-set-backup:checked ~ .settings-panels .settings-panel--backup,
|
||||
#hub-set-update:checked ~ .settings-panels .settings-panel--update {
|
||||
display: block;
|
||||
}
|
||||
.settings-grid {
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
(function () {
|
||||
function sleep(ms) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchJson(url, opts) {
|
||||
const r = await fetch(url, opts || {});
|
||||
let data = {};
|
||||
try {
|
||||
data = await r.json();
|
||||
} catch (e) {
|
||||
data = {};
|
||||
}
|
||||
return { httpOk: r.ok, status: r.status, data: data };
|
||||
}
|
||||
|
||||
function setStatus(el, text, cls) {
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "inst-update-status" + (cls ? " " + cls : "");
|
||||
}
|
||||
|
||||
function fmtVer(v) {
|
||||
if (!v || !v.online) return "离线";
|
||||
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
|
||||
const branch = v.branch ? v.branch + " · " : "";
|
||||
if (!short) return v.msg || "未知版本";
|
||||
return branch + short + (v.dirty ? " (dirty)" : "");
|
||||
}
|
||||
|
||||
async function refreshVersion(row) {
|
||||
const iid = row.getAttribute("data-id");
|
||||
const verEl = row.querySelector("[data-inst-version]");
|
||||
if (!iid || !verEl) return null;
|
||||
verEl.textContent = "探测中…";
|
||||
const res = await fetchJson("/api/instances/" + iid + "/version");
|
||||
const v = res.data || {};
|
||||
if (v.online) {
|
||||
verEl.textContent = fmtVer(v);
|
||||
verEl.className = "inst-version online";
|
||||
} else {
|
||||
verEl.textContent = (v.msg || "离线/不可达");
|
||||
verEl.className = "inst-version offline";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
async function waitForUpdate(row, beforeSha) {
|
||||
const statusEl = row.querySelector("[data-inst-update-status]");
|
||||
const iid = row.getAttribute("data-id");
|
||||
let sawDown = false;
|
||||
const maxTries = 45;
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
await sleep(2000);
|
||||
const res = await fetchJson("/api/instances/" + iid + "/version");
|
||||
const v = res.data || {};
|
||||
if (!v.online) {
|
||||
sawDown = true;
|
||||
setStatus(statusEl, "已受理 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||
continue;
|
||||
}
|
||||
const afterSha = v.sha || "";
|
||||
const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : "");
|
||||
const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : "";
|
||||
if (afterSha && beforeSha && afterSha !== beforeSha) {
|
||||
setStatus(
|
||||
statusEl,
|
||||
"更新完成: " + (beforeShort || "?") + " → " + (afterShort || "?"),
|
||||
"ok"
|
||||
);
|
||||
await refreshVersion(row);
|
||||
return true;
|
||||
}
|
||||
if (sawDown) {
|
||||
setStatus(
|
||||
statusEl,
|
||||
afterSha && beforeSha && afterSha === beforeSha
|
||||
? "已重启上线(已是最新 " + (afterShort || "") + ")"
|
||||
: "已上线 " + (afterShort || ""),
|
||||
"ok"
|
||||
);
|
||||
await refreshVersion(row);
|
||||
return true;
|
||||
}
|
||||
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||
}
|
||||
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查客户端日志", "err");
|
||||
await refreshVersion(row);
|
||||
return false;
|
||||
}
|
||||
|
||||
async function runUpdate(row) {
|
||||
const iid = row.getAttribute("data-id");
|
||||
const name = row.getAttribute("data-name") || ("#" + iid);
|
||||
const btn = row.querySelector("[data-inst-update]");
|
||||
const statusEl = row.querySelector("[data-inst-update-status]");
|
||||
if (!iid || !btn) return;
|
||||
const ok = window.confirm(
|
||||
"确认远程更新「" +
|
||||
name +
|
||||
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
|
||||
);
|
||||
if (!ok) return;
|
||||
|
||||
btn.disabled = true;
|
||||
setStatus(statusEl, "读取当前版本…", "pending");
|
||||
const before = await refreshVersion(row);
|
||||
const beforeSha = (before && before.sha) || "";
|
||||
|
||||
setStatus(statusEl, "下发更新指令…", "pending");
|
||||
const res = await fetchJson("/api/instances/" + iid + "/update", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
const data = res.data || {};
|
||||
if (!data.ok) {
|
||||
setStatus(statusEl, data.msg || "更新失败", "err");
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
setStatus(statusEl, "已受理 → 等待上线…", "pending");
|
||||
try {
|
||||
await waitForUpdate(row, beforeSha || data.before_sha || "");
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bind() {
|
||||
document.querySelectorAll(".inst-row[data-id]").forEach(function (row) {
|
||||
const updateBtn = row.querySelector("[data-inst-update]");
|
||||
const refreshBtn = row.querySelector("[data-inst-refresh-ver]");
|
||||
if (updateBtn) {
|
||||
updateBtn.addEventListener("click", function () {
|
||||
runUpdate(row);
|
||||
});
|
||||
}
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener("click", function () {
|
||||
refreshVersion(row);
|
||||
});
|
||||
}
|
||||
refreshVersion(row);
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", bind);
|
||||
} else {
|
||||
bind();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,275 @@
|
||||
(function (global) {
|
||||
function sleep(ms) {
|
||||
return new Promise(function (resolve) {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchJson(url, opts) {
|
||||
const r = await fetch(url, Object || {});
|
||||
let data = {};
|
||||
try {
|
||||
data = await r.json();
|
||||
} catch (e) {
|
||||
data = {};
|
||||
}
|
||||
return { httpOk: r.ok, status: r.status, data: data };
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
function fmtVer(v) {
|
||||
if (!v) return "—";
|
||||
if (v.online === false) return v.msg || "离线";
|
||||
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
|
||||
const branch = v.branch ? v.branch + " · " : "";
|
||||
if (!short) return v.msg || (v.ok ? "未知版本" : "不可用");
|
||||
return branch + short + (v.dirty ? " (dirty)" : "");
|
||||
}
|
||||
|
||||
function setStatus(el, text, cls) {
|
||||
if (!el) return;
|
||||
el.textContent = text || "";
|
||||
el.className = "update-status" + (cls ? " " + cls : "");
|
||||
}
|
||||
|
||||
async function waitRecover(versionUrl, beforeSha, statusEl, label) {
|
||||
let sawDown = false;
|
||||
const maxTries = 45;
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
await sleep(2000);
|
||||
const res = await fetchJson(versionUrl);
|
||||
const v = res.data || {};
|
||||
const online = res.httpOk && (v.ok !== false) && (v.online !== false);
|
||||
if (!online) {
|
||||
sawDown = true;
|
||||
setStatus(statusEl, "已受理 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||
continue;
|
||||
}
|
||||
const afterSha = v.sha || "";
|
||||
const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : "");
|
||||
const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : "";
|
||||
if (afterSha && beforeSha && afterSha !== beforeSha) {
|
||||
setStatus(statusEl, (label || "更新完成") + ": " + (beforeShort || "?") + " → " + (afterShort || "?"), "ok");
|
||||
return v;
|
||||
}
|
||||
if (sawDown) {
|
||||
setStatus(
|
||||
statusEl,
|
||||
afterSha && beforeSha && afterSha === beforeSha
|
||||
? "已重启上线(已是最新 " + (afterShort || "") + ")"
|
||||
: "已上线 " + (afterShort || ""),
|
||||
"ok"
|
||||
);
|
||||
return v;
|
||||
}
|
||||
setStatus(statusEl, "更新中… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||
}
|
||||
setStatus(statusEl, "超时未确认上线,请稍后刷新版本或查服务器日志", "err");
|
||||
return null;
|
||||
}
|
||||
|
||||
async function refreshHubVersion() {
|
||||
const verEl = document.getElementById("hub-update-version");
|
||||
if (!verEl) return null;
|
||||
verEl.textContent = "探测中…";
|
||||
verEl.className = "update-version";
|
||||
const res = await fetchJson("/api/settings/update/hub/version");
|
||||
const v = res.data || {};
|
||||
if (res.httpOk && v.ok) {
|
||||
verEl.textContent = fmtVer(v);
|
||||
verEl.className = "update-version online";
|
||||
} else {
|
||||
verEl.textContent = v.msg || "读取失败";
|
||||
verEl.className = "update-version offline";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
async function runHubUpdate() {
|
||||
const btn = document.getElementById("hub-update-btn");
|
||||
const statusEl = document.getElementById("hub-update-status");
|
||||
if (!btn) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"确认更新中控?\n\n将执行 git pull + pm2 restart。\n页面会短暂不可用,完成后自动探测版本。"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
setStatus(statusEl, "读取当前版本…", "pending");
|
||||
const before = await refreshHubVersion();
|
||||
const beforeSha = (before && before.sha) || "";
|
||||
setStatus(statusEl, "下发更新指令…", "pending");
|
||||
const res = await fetchJson("/api/settings/update/hub", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
const data = res.data || {};
|
||||
if (!data.ok) {
|
||||
setStatus(statusEl, data.msg || "更新失败", "err");
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
setStatus(statusEl, "已受理 → 等待上线…", "pending");
|
||||
try {
|
||||
await waitRecover(
|
||||
"/api/settings/update/hub/version",
|
||||
beforeSha || data.before_sha || "",
|
||||
statusEl,
|
||||
"中控更新完成"
|
||||
);
|
||||
await refreshHubVersion();
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshClientRow(row) {
|
||||
const iid = row.getAttribute("data-id");
|
||||
const verEl = row.querySelector("[data-client-version]");
|
||||
if (!iid || !verEl) return null;
|
||||
verEl.textContent = "探测中…";
|
||||
verEl.className = "update-version";
|
||||
const res = await fetchJson("/api/instances/" + iid + "/version");
|
||||
const v = res.data || {};
|
||||
if (v.online) {
|
||||
verEl.textContent = fmtVer(Object.assign({ online: true }, v));
|
||||
verEl.className = "update-version online";
|
||||
} else {
|
||||
verEl.textContent = v.msg || "离线/不可达";
|
||||
verEl.className = "update-version offline";
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
async function runClientUpdate(row) {
|
||||
const iid = row.getAttribute("data-id");
|
||||
const name = row.getAttribute("data-name") || ("#" + iid);
|
||||
const btn = row.querySelector("[data-client-update]");
|
||||
const statusEl = row.querySelector("[data-client-status]");
|
||||
if (!iid || !btn) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"确认远程更新「" +
|
||||
name +
|
||||
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
setStatus(statusEl, "读取当前版本…", "pending");
|
||||
const before = await refreshClientRow(row);
|
||||
const beforeSha = (before && before.sha) || "";
|
||||
setStatus(statusEl, "下发更新指令…", "pending");
|
||||
const res = await fetchJson("/api/instances/" + iid + "/update", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: "{}",
|
||||
});
|
||||
const data = res.data || {};
|
||||
if (!data.ok) {
|
||||
setStatus(statusEl, data.msg || "更新失败", "err");
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
setStatus(statusEl, "已受理 → 等待上线…", "pending");
|
||||
try {
|
||||
await waitRecover(
|
||||
"/api/instances/" + iid + "/version",
|
||||
beforeSha || data.before_sha || "",
|
||||
statusEl,
|
||||
"更新完成"
|
||||
);
|
||||
await refreshClientRow(row);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClients() {
|
||||
const body = document.getElementById("client-update-body");
|
||||
if (!body) return;
|
||||
body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
||||
try {
|
||||
const res = await fetchJson("/api/settings/update/clients");
|
||||
const items = (res.data && res.data.items) || [];
|
||||
if (!items.length) {
|
||||
body.innerHTML = '<tr><td colspan="4" class="muted">暂无登记实例</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = items
|
||||
.map(function (it) {
|
||||
const id = it.id;
|
||||
const name = it.name || "#" + id;
|
||||
const enabled = !!it.enabled;
|
||||
return (
|
||||
'<tr class="client-update-row" data-id="' +
|
||||
escapeHtml(String(id)) +
|
||||
'" data-name="' +
|
||||
escapeHtml(name) +
|
||||
'">' +
|
||||
"<td><strong>" +
|
||||
escapeHtml(name) +
|
||||
"</strong><div class=\"muted\" style=\"font-size:.75rem\">" +
|
||||
escapeHtml(it.base_url || "") +
|
||||
"</div></td>" +
|
||||
'<td><span class="update-version muted" data-client-version>—</span></td>' +
|
||||
"<td>" +
|
||||
(enabled ? "启用" : '<span class="muted">停用</span>') +
|
||||
"</td>" +
|
||||
'<td style="white-space:nowrap">' +
|
||||
'<button type="button" class="btn btn-sm" data-client-update' +
|
||||
(enabled ? "" : " disabled title=\"实例未启用\"") +
|
||||
">远程更新</button> " +
|
||||
'<button type="button" class="btn btn-sm btn-secondary" data-client-refresh>刷新版本</button>' +
|
||||
' <span class="update-status muted" data-client-status></span>' +
|
||||
"</td>" +
|
||||
"</tr>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
|
||||
body.querySelectorAll(".client-update-row").forEach(function (row) {
|
||||
const upd = row.querySelector("[data-client-update]");
|
||||
const ref = row.querySelector("[data-client-refresh]");
|
||||
if (upd) {
|
||||
upd.addEventListener("click", function () {
|
||||
runClientUpdate(row);
|
||||
});
|
||||
}
|
||||
if (ref) {
|
||||
ref.addEventListener("click", function () {
|
||||
refreshClientRow(row);
|
||||
});
|
||||
}
|
||||
refreshClientRow(row);
|
||||
});
|
||||
} catch (e) {
|
||||
body.innerHTML =
|
||||
'<tr><td colspan="4" class="muted">' + escapeHtml((e && e.message) || "加载失败") + "</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
function boot() {
|
||||
const hubBtn = document.getElementById("hub-update-btn");
|
||||
const hubRefresh = document.getElementById("hub-update-refresh");
|
||||
const clientsRefresh = document.getElementById("client-update-refresh");
|
||||
if (hubBtn) hubBtn.addEventListener("click", runHubUpdate);
|
||||
if (hubRefresh) hubRefresh.addEventListener("click", refreshHubVersion);
|
||||
if (clientsRefresh) clientsRefresh.addEventListener("click", loadClients);
|
||||
refreshHubVersion();
|
||||
loadClients();
|
||||
}
|
||||
|
||||
global.HubSettingsUpdate = { boot: boot };
|
||||
})(window);
|
||||
Reference in New Issue
Block a user