系统更新增加客户端「全部更新」,依次远程更新所有启用实例。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+400
-339
@@ -1,339 +1,400 @@
|
|||||||
(function (global) {
|
(function (global) {
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
setTimeout(resolve, ms);
|
setTimeout(resolve, ms);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url, opts) {
|
async function fetchJson(url, opts) {
|
||||||
const base = opts || {};
|
const base = opts || {};
|
||||||
const headers = Object.assign({ Accept: "application/json" }, base.headers || {});
|
const headers = Object.assign({ Accept: "application/json" }, base.headers || {});
|
||||||
try {
|
try {
|
||||||
const r = await fetch(
|
const r = await fetch(
|
||||||
url,
|
url,
|
||||||
Object.assign({}, base, {
|
Object.assign({}, base, {
|
||||||
credentials: "same-origin",
|
credentials: "same-origin",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
let data = {};
|
let data = {};
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : {};
|
data = text ? JSON.parse(text) : {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data = {
|
data = {
|
||||||
ok: false,
|
ok: false,
|
||||||
msg: "非 JSON 响应 HTTP " + r.status + (text ? ": " + String(text).slice(0, 120) : ""),
|
msg: "非 JSON 响应 HTTP " + r.status + (text ? ": " + String(text).slice(0, 120) : ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!data || typeof data !== "object") data = {};
|
if (!data || typeof data !== "object") data = {};
|
||||||
if (data.ok == null && !r.ok) {
|
if (data.ok == null && !r.ok) {
|
||||||
data.ok = false;
|
data.ok = false;
|
||||||
data.msg = data.msg || "请求失败 HTTP " + r.status;
|
data.msg = data.msg || "请求失败 HTTP " + r.status;
|
||||||
}
|
}
|
||||||
return { httpOk: r.ok, status: r.status, data: data };
|
return { httpOk: r.ok, status: r.status, data: data };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
httpOk: false,
|
httpOk: false,
|
||||||
status: 0,
|
status: 0,
|
||||||
data: { ok: false, msg: (e && e.message) || "网络错误" },
|
data: { ok: false, msg: (e && e.message) || "网络错误" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s)
|
return String(s)
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fmtVer(v) {
|
function fmtVer(v) {
|
||||||
if (!v) return "—";
|
if (!v) return "—";
|
||||||
if (v.online === false) return v.msg || "离线";
|
if (v.online === false) return v.msg || "离线";
|
||||||
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
|
const short = v.short_sha || (v.sha ? String(v.sha).slice(0, 7) : "");
|
||||||
const branch = v.branch ? v.branch + " · " : "";
|
const branch = v.branch ? v.branch + " · " : "";
|
||||||
if (!short) return v.msg || (v.ok ? "未知版本" : "不可用");
|
if (!short) return v.msg || (v.ok ? "未知版本" : "不可用");
|
||||||
return branch + short + (v.dirty ? " (dirty)" : "");
|
return branch + short + (v.dirty ? " (dirty)" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(el, text, cls) {
|
function setStatus(el, text, cls) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.textContent = text || "";
|
el.textContent = text || "";
|
||||||
el.className = "update-status" + (cls ? " " + cls : "");
|
el.className = "update-status" + (cls ? " " + cls : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitRecover(versionUrl, beforeSha, statusEl, label, opts) {
|
async function waitRecover(versionUrl, beforeSha, statusEl, label, opts) {
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
const expectSha = options.expectSha || "";
|
const expectSha = options.expectSha || "";
|
||||||
const alreadyLatest = !!options.alreadyLatest;
|
const alreadyLatest = !!options.alreadyLatest;
|
||||||
let sawDown = false;
|
let sawDown = false;
|
||||||
let stableOnline = 0;
|
let stableOnline = 0;
|
||||||
const maxTries = 30;
|
const maxTries = 30;
|
||||||
for (let i = 0; i < maxTries; i++) {
|
for (let i = 0; i < maxTries; i++) {
|
||||||
await sleep(1500);
|
await sleep(1500);
|
||||||
const res = await fetchJson(versionUrl);
|
const res = await fetchJson(versionUrl);
|
||||||
const v = res.data || {};
|
const v = res.data || {};
|
||||||
const online = res.httpOk && (v.ok !== false) && (v.online !== false);
|
const online = res.httpOk && (v.ok !== false) && (v.online !== false);
|
||||||
if (!online) {
|
if (!online) {
|
||||||
sawDown = true;
|
sawDown = true;
|
||||||
stableOnline = 0;
|
stableOnline = 0;
|
||||||
setStatus(statusEl, "重启中 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
setStatus(statusEl, "重启中 → 等待上线… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const afterSha = v.sha || "";
|
const afterSha = v.sha || "";
|
||||||
const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : "");
|
const afterShort = v.short_sha || (afterSha ? afterSha.slice(0, 7) : "");
|
||||||
const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : "";
|
const beforeShort = beforeSha ? String(beforeSha).slice(0, 7) : "";
|
||||||
if (expectSha && afterSha && afterSha === expectSha) {
|
if (expectSha && afterSha && afterSha === expectSha) {
|
||||||
setStatus(
|
setStatus(
|
||||||
statusEl,
|
statusEl,
|
||||||
(label || "更新完成") +
|
(label || "更新完成") +
|
||||||
(beforeSha && beforeSha !== afterSha
|
(beforeSha && beforeSha !== afterSha
|
||||||
? ": " + (beforeShort || "?") + " → " + (afterShort || "?")
|
? ": " + (beforeShort || "?") + " → " + (afterShort || "?")
|
||||||
: " " + (afterShort || "")),
|
: " " + (afterShort || "")),
|
||||||
"ok"
|
"ok"
|
||||||
);
|
);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
if (afterSha && beforeSha && afterSha !== beforeSha) {
|
if (afterSha && beforeSha && afterSha !== beforeSha) {
|
||||||
setStatus(statusEl, (label || "更新完成") + ": " + (beforeShort || "?") + " → " + (afterShort || "?"), "ok");
|
setStatus(statusEl, (label || "更新完成") + ": " + (beforeShort || "?") + " → " + (afterShort || "?"), "ok");
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
if (sawDown) {
|
if (sawDown) {
|
||||||
setStatus(
|
setStatus(
|
||||||
statusEl,
|
statusEl,
|
||||||
afterSha && beforeSha && afterSha === beforeSha
|
afterSha && beforeSha && afterSha === beforeSha
|
||||||
? "已重启上线(已是最新 " + (afterShort || "") + ")"
|
? "已重启上线(已是最新 " + (afterShort || "") + ")"
|
||||||
: "已上线 " + (afterShort || ""),
|
: "已上线 " + (afterShort || ""),
|
||||||
"ok"
|
"ok"
|
||||||
);
|
);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
stableOnline += 1;
|
stableOnline += 1;
|
||||||
// 重启很快时可能看不到掉线:连续在线数次且版本符合预期/未变 → 视为完成
|
// 重启很快时可能看不到掉线:连续在线数次且版本符合预期/未变 → 视为完成
|
||||||
if (stableOnline >= 4) {
|
if (stableOnline >= 4) {
|
||||||
if (alreadyLatest || (afterSha && beforeSha && afterSha === beforeSha)) {
|
if (alreadyLatest || (afterSha && beforeSha && afterSha === beforeSha)) {
|
||||||
setStatus(statusEl, "完成(已是最新 " + (afterShort || "") + ",进程已重启)", "ok");
|
setStatus(statusEl, "完成(已是最新 " + (afterShort || "") + ",进程已重启)", "ok");
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
if (expectSha && afterSha === expectSha) {
|
if (expectSha && afterSha === expectSha) {
|
||||||
setStatus(statusEl, (label || "更新完成") + " " + (afterShort || ""), "ok");
|
setStatus(statusEl, (label || "更新完成") + " " + (afterShort || ""), "ok");
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setStatus(statusEl, "等待重启生效… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
setStatus(statusEl, "等待重启生效… (" + (i + 1) + "/" + maxTries + ")", "pending");
|
||||||
}
|
}
|
||||||
setStatus(statusEl, "超时未确认,请点「刷新版本」或查看 logs/hub_update.log", "err");
|
setStatus(statusEl, "超时未确认,请点「刷新版本」或查看 logs/hub_update.log", "err");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshHubVersion() {
|
async function refreshHubVersion() {
|
||||||
const verEl = document.getElementById("hub-update-version");
|
const verEl = document.getElementById("hub-update-version");
|
||||||
if (!verEl) return null;
|
if (!verEl) return null;
|
||||||
verEl.textContent = "探测中…";
|
verEl.textContent = "探测中…";
|
||||||
verEl.className = "update-version";
|
verEl.className = "update-version";
|
||||||
const res = await fetchJson("/api/settings/update/hub/version");
|
const res = await fetchJson("/api/settings/update/hub/version");
|
||||||
const v = res.data || {};
|
const v = res.data || {};
|
||||||
if (res.httpOk && v.ok) {
|
if (res.httpOk && v.ok) {
|
||||||
verEl.textContent = fmtVer(v);
|
verEl.textContent = fmtVer(v);
|
||||||
verEl.className = "update-version online";
|
verEl.className = "update-version online";
|
||||||
} else {
|
} else {
|
||||||
verEl.textContent = v.msg || "读取失败";
|
verEl.textContent = v.msg || "读取失败";
|
||||||
verEl.className = "update-version offline";
|
verEl.className = "update-version offline";
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHubUpdate() {
|
async function runHubUpdate() {
|
||||||
const btn = document.getElementById("hub-update-btn");
|
const btn = document.getElementById("hub-update-btn");
|
||||||
const statusEl = document.getElementById("hub-update-status");
|
const statusEl = document.getElementById("hub-update-status");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
if (
|
if (
|
||||||
!window.confirm(
|
!window.confirm(
|
||||||
"确认更新中控?\n\n将执行 git pull + pm2 restart。\n页面会短暂不可用,完成后自动探测版本。"
|
"确认更新中控?\n\n将执行 git pull + pm2 restart。\n页面会短暂不可用,完成后自动探测版本。"
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
setStatus(statusEl, "读取当前版本…", "pending");
|
setStatus(statusEl, "读取当前版本…", "pending");
|
||||||
const before = await refreshHubVersion();
|
const before = await refreshHubVersion();
|
||||||
const beforeSha = (before && before.sha) || "";
|
const beforeSha = (before && before.sha) || "";
|
||||||
setStatus(statusEl, "下发更新指令…", "pending");
|
setStatus(statusEl, "下发更新指令…", "pending");
|
||||||
const res = await fetchJson("/api/settings/update/hub", {
|
const res = await fetchJson("/api/settings/update/hub", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
body: "{}",
|
body: "{}",
|
||||||
});
|
});
|
||||||
const data = res.data || {};
|
const data = res.data || {};
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
const detail =
|
const detail =
|
||||||
data.msg ||
|
data.msg ||
|
||||||
(res.status ? "更新失败 HTTP " + res.status : "更新失败(无响应)");
|
(res.status ? "更新失败 HTTP " + res.status : "更新失败(无响应)");
|
||||||
setStatus(statusEl, detail, "err");
|
setStatus(statusEl, detail, "err");
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setStatus(statusEl, data.msg || "已拉取,等待重启…", "pending");
|
setStatus(statusEl, data.msg || "已拉取,等待重启…", "pending");
|
||||||
try {
|
try {
|
||||||
await waitRecover(
|
await waitRecover(
|
||||||
"/api/settings/update/hub/version",
|
"/api/settings/update/hub/version",
|
||||||
beforeSha || data.before_sha || "",
|
beforeSha || data.before_sha || "",
|
||||||
statusEl,
|
statusEl,
|
||||||
"中控更新完成",
|
"中控更新完成",
|
||||||
{
|
{
|
||||||
expectSha: data.after_sha || "",
|
expectSha: data.after_sha || "",
|
||||||
alreadyLatest: !data.changed,
|
alreadyLatest: !data.changed,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
await refreshHubVersion();
|
await refreshHubVersion();
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshClientRow(row) {
|
async function refreshClientRow(row) {
|
||||||
const iid = row.getAttribute("data-id");
|
const iid = row.getAttribute("data-id");
|
||||||
const verEl = row.querySelector("[data-client-version]");
|
const verEl = row.querySelector("[data-client-version]");
|
||||||
if (!iid || !verEl) return null;
|
if (!iid || !verEl) return null;
|
||||||
verEl.textContent = "探测中…";
|
verEl.textContent = "探测中…";
|
||||||
verEl.className = "update-version";
|
verEl.className = "update-version";
|
||||||
const res = await fetchJson("/api/instances/" + iid + "/version");
|
const res = await fetchJson("/api/instances/" + iid + "/version");
|
||||||
const v = res.data || {};
|
const v = res.data || {};
|
||||||
if (v.online) {
|
if (v.online) {
|
||||||
verEl.textContent = fmtVer(Object.assign({ online: true }, v));
|
verEl.textContent = fmtVer(Object.assign({ online: true }, v));
|
||||||
verEl.className = "update-version online";
|
verEl.className = "update-version online";
|
||||||
} else {
|
} else {
|
||||||
verEl.textContent = v.msg || "离线/不可达";
|
verEl.textContent = v.msg || "离线/不可达";
|
||||||
verEl.className = "update-version offline";
|
verEl.className = "update-version offline";
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runClientUpdate(row) {
|
async function runClientUpdate(row, opts) {
|
||||||
const iid = row.getAttribute("data-id");
|
const options = opts || {};
|
||||||
const name = row.getAttribute("data-name") || ("#" + iid);
|
const iid = row.getAttribute("data-id");
|
||||||
const btn = row.querySelector("[data-client-update]");
|
const name = row.getAttribute("data-name") || ("#" + iid);
|
||||||
const statusEl = row.querySelector("[data-client-status]");
|
const btn = row.querySelector("[data-client-update]");
|
||||||
if (!iid || !btn) return;
|
const statusEl = row.querySelector("[data-client-status]");
|
||||||
if (
|
if (!iid || !btn) return { ok: false, name: name, msg: "无效行" };
|
||||||
!window.confirm(
|
if (btn.disabled && !options.forceEnabled) {
|
||||||
"确认远程更新「" +
|
return { ok: false, name: name, msg: "未启用或忙碌中" };
|
||||||
name +
|
}
|
||||||
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
|
if (
|
||||||
)
|
!options.skipConfirm &&
|
||||||
) {
|
!window.confirm(
|
||||||
return;
|
"确认远程更新「" +
|
||||||
}
|
name +
|
||||||
btn.disabled = true;
|
"」?\n\n将执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
|
||||||
setStatus(statusEl, "读取当前版本…", "pending");
|
)
|
||||||
const before = await refreshClientRow(row);
|
) {
|
||||||
const beforeSha = (before && before.sha) || "";
|
return { ok: false, name: name, msg: "已取消" };
|
||||||
setStatus(statusEl, "下发更新指令…", "pending");
|
}
|
||||||
const res = await fetchJson("/api/instances/" + iid + "/update", {
|
btn.disabled = true;
|
||||||
method: "POST",
|
setStatus(statusEl, "读取当前版本…", "pending");
|
||||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
const before = await refreshClientRow(row);
|
||||||
body: "{}",
|
const beforeSha = (before && before.sha) || "";
|
||||||
});
|
setStatus(statusEl, "下发更新指令…", "pending");
|
||||||
const data = res.data || {};
|
const res = await fetchJson("/api/instances/" + iid + "/update", {
|
||||||
if (!data.ok) {
|
method: "POST",
|
||||||
setStatus(
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||||
statusEl,
|
body: "{}",
|
||||||
data.msg || (res.status ? "更新失败 HTTP " + res.status : "更新失败"),
|
});
|
||||||
"err"
|
const data = res.data || {};
|
||||||
);
|
if (!data.ok) {
|
||||||
btn.disabled = false;
|
const msg =
|
||||||
return;
|
data.msg || (res.status ? "更新失败 HTTP " + res.status : "更新失败");
|
||||||
}
|
setStatus(statusEl, msg, "err");
|
||||||
setStatus(statusEl, "已受理 → 等待上线…", "pending");
|
btn.disabled = false;
|
||||||
try {
|
return { ok: false, name: name, msg: msg };
|
||||||
await waitRecover(
|
}
|
||||||
"/api/instances/" + iid + "/version",
|
setStatus(statusEl, "已受理 → 等待上线…", "pending");
|
||||||
beforeSha || data.before_sha || "",
|
try {
|
||||||
statusEl,
|
await waitRecover(
|
||||||
"更新完成"
|
"/api/instances/" + iid + "/version",
|
||||||
);
|
beforeSha || data.before_sha || "",
|
||||||
await refreshClientRow(row);
|
statusEl,
|
||||||
} finally {
|
"更新完成"
|
||||||
btn.disabled = false;
|
);
|
||||||
}
|
await refreshClientRow(row);
|
||||||
}
|
return { ok: true, name: name };
|
||||||
|
} finally {
|
||||||
async function loadClients() {
|
btn.disabled = false;
|
||||||
const body = document.getElementById("client-update-body");
|
}
|
||||||
if (!body) return;
|
}
|
||||||
body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
|
||||||
try {
|
async function runAllClientUpdates() {
|
||||||
const res = await fetchJson("/api/settings/update/clients");
|
const allBtn = document.getElementById("client-update-all");
|
||||||
const items = (res.data && res.data.items) || [];
|
const allStatus = document.getElementById("client-update-all-status");
|
||||||
if (!items.length) {
|
const rows = Array.prototype.slice
|
||||||
body.innerHTML = '<tr><td colspan="4" class="muted">暂无登记实例</td></tr>';
|
.call(document.querySelectorAll(".client-update-row"))
|
||||||
return;
|
.filter(function (row) {
|
||||||
}
|
const btn = row.querySelector("[data-client-update]");
|
||||||
body.innerHTML = items
|
return btn && !btn.disabled;
|
||||||
.map(function (it) {
|
});
|
||||||
const id = it.id;
|
if (!rows.length) {
|
||||||
const name = it.name || "#" + id;
|
setStatus(allStatus, "没有可更新的启用实例", "err");
|
||||||
const enabled = !!it.enabled;
|
return;
|
||||||
return (
|
}
|
||||||
'<tr class="client-update-row" data-id="' +
|
if (
|
||||||
escapeHtml(String(id)) +
|
!window.confirm(
|
||||||
'" data-name="' +
|
"确认远程更新全部 " +
|
||||||
escapeHtml(name) +
|
rows.length +
|
||||||
'">' +
|
" 台启用实例?\n\n将依次执行 git pull + pm2 restart。\n有持仓时服务会短暂中断,请谨慎。"
|
||||||
"<td><strong>" +
|
)
|
||||||
escapeHtml(name) +
|
) {
|
||||||
"</strong><div class=\"muted\" style=\"font-size:.75rem\">" +
|
return;
|
||||||
escapeHtml(it.base_url || "") +
|
}
|
||||||
"</div></td>" +
|
if (allBtn) allBtn.disabled = true;
|
||||||
'<td><span class="update-version muted" data-client-version>—</span></td>' +
|
const refreshBtn = document.getElementById("client-update-refresh");
|
||||||
"<td>" +
|
if (refreshBtn) refreshBtn.disabled = true;
|
||||||
(enabled ? "启用" : '<span class="muted">停用</span>') +
|
let okCount = 0;
|
||||||
"</td>" +
|
let failCount = 0;
|
||||||
'<td style="white-space:nowrap">' +
|
try {
|
||||||
'<button type="button" class="btn btn-sm" data-client-update' +
|
for (let i = 0; i < rows.length; i++) {
|
||||||
(enabled ? "" : " disabled title=\"实例未启用\"") +
|
const row = rows[i];
|
||||||
">远程更新</button> " +
|
const name = row.getAttribute("data-name") || "#" + (i + 1);
|
||||||
'<button type="button" class="btn btn-sm btn-secondary" data-client-refresh>刷新版本</button>' +
|
setStatus(
|
||||||
' <span class="update-status muted" data-client-status></span>' +
|
allStatus,
|
||||||
"</td>" +
|
"全部更新中 " + (i + 1) + "/" + rows.length + " · " + name,
|
||||||
"</tr>"
|
"pending"
|
||||||
);
|
);
|
||||||
})
|
const result = await runClientUpdate(row, { skipConfirm: true });
|
||||||
.join("");
|
if (result && result.ok) okCount += 1;
|
||||||
|
else failCount += 1;
|
||||||
body.querySelectorAll(".client-update-row").forEach(function (row) {
|
}
|
||||||
const upd = row.querySelector("[data-client-update]");
|
if (failCount === 0) {
|
||||||
const ref = row.querySelector("[data-client-refresh]");
|
setStatus(allStatus, "全部更新完成 · " + okCount + " 台", "ok");
|
||||||
if (upd) {
|
} else {
|
||||||
upd.addEventListener("click", function () {
|
setStatus(
|
||||||
runClientUpdate(row);
|
allStatus,
|
||||||
});
|
"全部更新结束 · 成功 " + okCount + " · 失败 " + failCount,
|
||||||
}
|
"err"
|
||||||
if (ref) {
|
);
|
||||||
ref.addEventListener("click", function () {
|
}
|
||||||
refreshClientRow(row);
|
} finally {
|
||||||
});
|
if (allBtn) allBtn.disabled = false;
|
||||||
}
|
if (refreshBtn) refreshBtn.disabled = false;
|
||||||
refreshClientRow(row);
|
}
|
||||||
});
|
}
|
||||||
} catch (e) {
|
|
||||||
body.innerHTML =
|
async function loadClients() {
|
||||||
'<tr><td colspan="4" class="muted">' + escapeHtml((e && e.message) || "加载失败") + "</td></tr>";
|
const body = document.getElementById("client-update-body");
|
||||||
}
|
if (!body) return;
|
||||||
}
|
body.innerHTML = '<tr><td colspan="4" class="muted">加载中…</td></tr>';
|
||||||
|
try {
|
||||||
function boot() {
|
const res = await fetchJson("/api/settings/update/clients");
|
||||||
const hubBtn = document.getElementById("hub-update-btn");
|
const items = (res.data && res.data.items) || [];
|
||||||
const hubRefresh = document.getElementById("hub-update-refresh");
|
if (!items.length) {
|
||||||
const clientsRefresh = document.getElementById("client-update-refresh");
|
body.innerHTML = '<tr><td colspan="4" class="muted">暂无登记实例</td></tr>';
|
||||||
if (hubBtn) hubBtn.addEventListener("click", runHubUpdate);
|
return;
|
||||||
if (hubRefresh) hubRefresh.addEventListener("click", refreshHubVersion);
|
}
|
||||||
if (clientsRefresh) clientsRefresh.addEventListener("click", loadClients);
|
body.innerHTML = items
|
||||||
refreshHubVersion();
|
.map(function (it) {
|
||||||
loadClients();
|
const id = it.id;
|
||||||
}
|
const name = it.name || "#" + id;
|
||||||
|
const enabled = !!it.enabled;
|
||||||
global.HubSettingsUpdate = { boot: boot };
|
return (
|
||||||
})(window);
|
'<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");
|
||||||
|
const clientsAll = document.getElementById("client-update-all");
|
||||||
|
if (hubBtn) hubBtn.addEventListener("click", runHubUpdate);
|
||||||
|
if (hubRefresh) hubRefresh.addEventListener("click", refreshHubVersion);
|
||||||
|
if (clientsRefresh) clientsRefresh.addEventListener("click", loadClients);
|
||||||
|
if (clientsAll) clientsAll.addEventListener("click", runAllClientUpdates);
|
||||||
|
refreshHubVersion();
|
||||||
|
loadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.HubSettingsUpdate = { boot: boot };
|
||||||
|
})(window);
|
||||||
|
|||||||
@@ -166,7 +166,9 @@
|
|||||||
<h3>客户端远程更新</h3>
|
<h3>客户端远程更新</h3>
|
||||||
<p class="muted field-hint">经 Bridge 探测版本并下发更新;客户端需已具备 <code>/api/hub/update</code></p>
|
<p class="muted field-hint">经 Bridge 探测版本并下发更新;客户端需已具备 <code>/api/hub/update</code></p>
|
||||||
<div class="settings-actions">
|
<div class="settings-actions">
|
||||||
|
<button type="button" class="settings-primary" id="client-update-all">全部更新</button>
|
||||||
<button type="button" class="btn-secondary" id="client-update-refresh">刷新列表</button>
|
<button type="button" class="btn-secondary" id="client-update-refresh">刷新列表</button>
|
||||||
|
<span class="update-status muted" id="client-update-all-status"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -183,7 +185,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=4"></script>
|
<script src="{{ url_for('static', filename='settings_update.js') }}?v=5"></script>
|
||||||
<script>
|
<script>
|
||||||
HubSettingsBackup.boot();
|
HubSettingsBackup.boot();
|
||||||
HubSettingsUpdate.boot();
|
HubSettingsUpdate.boot();
|
||||||
|
|||||||
Reference in New Issue
Block a user