(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(); } })();