(function (global) { function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } async function fetchJson(url, opts) { const base = opts || {}; const headers = Object.assign({ Accept: "application/json" }, base.headers || {}); try { 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) { return { httpOk: false, status: 0, data: { ok: false, msg: (e && e.message) || "网络错误" }, }; } } function escapeHtml(s) { return String(s) .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, "超时未确认上线,请稍后刷新版本或查看 logs/hub_update.log", "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) { const detail = data.msg || (res.status ? "更新失败 HTTP " + res.status : "更新失败(无响应)"); setStatus(statusEl, detail, "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 || (res.status ? "更新失败 HTTP " + res.status : "更新失败"), "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 = '