mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 05:57:39 +02:00
fix: stop a finished check from freezing the update console's source form
Reported from the live installation: after opening 系统设置 → 一键更新 (the automatic check runs) the 保存更新源 button silently stopped working — a click produced no request at all and the server log never saw a POST. Root cause: the busy flag is a render input — it stamps `disabled` + `data-state-disabled` on the action buttons — but the console only ever re-rendered *while* busy. checkForUpdates (including the automatic one) renders on entry and again when the response lands, and withUpdateBusy never rendered once the flag cleared, so the last render left the source-save button disabled for good. The carrier makes it permanent: data-state-disabled tells the platform's permission gate not to re-enable the element either. Measured in a browser after the auto-check: the DOM carried `disabled data-state-disabled="true"` while updateBusy was already false. Fix: withUpdateBusy now renders symmetrically — once on entry (the form visibly locks while an action is in flight) and once after the flag clears (usable again, button re-enabled) — skipping the exit render only when the restart watchdog owns the view. The transient error banner stops being an injected div and becomes state (updateActionError) rendered by renderUpdateConsole, so the exit render cannot wipe it; it clears when a new action starts or the console is re-entered. Tests: the regression follows the operator's exact chain — deferred automatic check → the form must be enabled again → clear the address → save must reach the server — plus a second-save check; the first fails on the old code on the disabled markup. Verified end to end in a real browser against a scratch installation: the render trace reads entry(busy)→response(busy)→exit(idle), and both saves (set and clear) hit the API and land in config.yaml.
This commit is contained in:
1 parent
069abc0a7c
commit
22a069849c
2 files changed
+57
-10
No files matched your search
@@ -1137,6 +1137,42 @@ test('the source section shows what is in effect, and saving posts the one addre
|
||||
assert.ok(h.toasts.some(t => t.type === 'success'), 'a saved source must be confirmed');
|
||||
});
|
||||
|
||||
test('a finished check must not leave the source form frozen (the button stays clickable)', async () => {
|
||||
// 线上实测过的形态:打开页面 → 自动检查在忙期里重绘 → 检查结束后不再重绘,
|
||||
// 「保存更新源」带着 disabled + data-state-disabled 留在 DOM 上——之后点击永远没有反应,
|
||||
// 服务端连请求都收不到。这里把用户的整条动作链原样跑一遍。
|
||||
const h = await harness({ status: statusOf({ installed: true }), source: configuredSource });
|
||||
|
||||
h.fireDeferred(); // 打开页面 1.2 秒后的那次自动检查
|
||||
assert.match(h.html(), /update-source-save-btn" disabled/,
|
||||
'the form must visibly lock while a check is in flight');
|
||||
await h.flush();
|
||||
assert.match(h.html(), /已是最新|落后/, 'precondition: the automatic check finished');
|
||||
assert.doesNotMatch(h.html(), /update-source-save-btn" disabled/,
|
||||
'a finished check must re-render the form with the button enabled again');
|
||||
|
||||
// 用户的下一步:清空地址 → 点保存 → 请求必须真的发出去
|
||||
h.runHandler("updateSourceRepoChanged('')");
|
||||
await h.fire('saveUpdateSource');
|
||||
await h.flush();
|
||||
const save = h.calls.find(c => c.url === '/api/system/update/source');
|
||||
assert.ok(save, 'saving right after a check must reach the server');
|
||||
assert.deepEqual(JSON.parse(save.body), { repo: '' });
|
||||
assert.doesNotMatch(h.html(), /update-source-save-btn" disabled/,
|
||||
'after saving the button must be ready for the next action, not frozen');
|
||||
});
|
||||
|
||||
test('a second save is not swallowed by a stale busy flag', async () => {
|
||||
const h = await harness({ status: statusOf({ installed: true }), source: configuredSource });
|
||||
h.runHandler("updateSourceRepoChanged('https://gitlab.com/mirror/csai.git')");
|
||||
await h.fire('saveUpdateSource');
|
||||
await h.flush();
|
||||
await h.fire('saveUpdateSource');
|
||||
await h.flush();
|
||||
const saves = h.calls.filter(c => c.url === '/api/system/update/source');
|
||||
assert.equal(saves.length, 2, 'the second save must also reach the server');
|
||||
});
|
||||
|
||||
test('clearing the address is the way back to the official repository', async () => {
|
||||
const h = await harness({ status: statusOf({ installed: true }), source: configuredSource });
|
||||
h.runHandler('updateSourceRepoChanged("")');
|
||||
|
||||
+21
-10
@@ -34,6 +34,7 @@ let updateWatchdogSlow = null; // null=还没超时;'running'=仍由旧
|
||||
let updateAutoCheckDone = false; // 每个会话只自动查一次远端,不每次重绘都去打扰
|
||||
let updateSourceDraft = null; // 编辑中的更新源仓库地址;null=还贴着服务端保存的值
|
||||
let updateSourceError = ''; // 最近一次保存/接入失败的原因,画在区块里
|
||||
let updateActionError = ''; // 最近一次动作(检查/更新/回滚/重启)失败的原因,画在控制台顶部
|
||||
let updateAdoptPlan = null; // 预览回来的接入计划;确认后清掉
|
||||
|
||||
function updateT(key, opts) {
|
||||
@@ -172,25 +173,30 @@ function notify(message, type) {
|
||||
}
|
||||
}
|
||||
|
||||
// 动作期的重绘必须头尾对称。忙时要画"占用中"(按钮带 disabled),闲时要再画回来——
|
||||
// 这个页面的检查流程(包括打开页面后的自动检查)会在忙期里重绘,若结束时不再重绘,
|
||||
// 那份 disabled 就留在 DOM 上把按钮永久冻死:实测打开页面自动检查一次后,「保存更新源」
|
||||
// 带 disabled + data-state-disabled 留在页面上,再点永远没有反应,连请求都发不出去。
|
||||
// 横幅改成状态(updateActionError)由 renderUpdateConsole 渲染,退出重绘就不会把它擦掉;
|
||||
// 重启守侧接管画面时(updateRestartPending)一律不重绘,别把自恢复视图盖回控制台。
|
||||
async function withUpdateBusy(fn, label) {
|
||||
if (updateBusy) return;
|
||||
updateBusy = true;
|
||||
updateActionError = '';
|
||||
if (!updateRestartPending) renderUpdateConsole();
|
||||
try {
|
||||
await fn();
|
||||
} catch (err) {
|
||||
const reason = err && err.message ? err.message : String(err);
|
||||
const text = updateT(UPDATE_FAILURE_KEYS[label] || 'loadFailed', { reason: reason });
|
||||
const el = document.getElementById('update-console');
|
||||
if (el && isUpdateConsoleActive()) {
|
||||
// 先按当前状态重绘再挂横幅:请求炸在半路上时,画面还停在"正在检查远端…"这种
|
||||
// 已经不再成立的状态上,光加一条红字是盖不住它的。
|
||||
renderUpdateConsole();
|
||||
el.insertAdjacentHTML('afterbegin', '<div class="update-error">' + escapeHtml(text) + '</div>');
|
||||
}
|
||||
// 内联横幅会被下一次轮询重绘冲掉,toast 才是留得住的那一份。
|
||||
notify(text, 'error');
|
||||
// 请求炸在半路上时,画面还停在"正在检查更新源…"这种已经不再成立的状态上,
|
||||
// 光加一条红字没用:横幅跟随状态重绘,画面本身也要按当前状态画一遍。
|
||||
updateActionError = updateT(UPDATE_FAILURE_KEYS[label] || 'loadFailed', { reason: reason });
|
||||
if (!updateRestartPending) renderUpdateConsole();
|
||||
// 内联横幅会被下一次动作开始时的重绘冲掉,toast 才是留得住的那一份。
|
||||
notify(updateActionError, 'error');
|
||||
} finally {
|
||||
updateBusy = false;
|
||||
if (!updateRestartPending) renderUpdateConsole();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +214,7 @@ async function loadUpdateConsole() {
|
||||
resetUpdateCheck();
|
||||
updateRestartChoiceSet = false;
|
||||
updateRestartChoice = true; // 之后由 noteRestartDefault 按 supervised 修正
|
||||
updateActionError = ''; // 重新进页 = 干净画面,上一次动作的红字不跟过来
|
||||
const el = document.getElementById('update-console');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="empty-state">' + escapeHtml(updateT('loading')) + '</div>';
|
||||
@@ -957,6 +964,10 @@ function renderUpdateConsole() {
|
||||
const job = updateJobOf();
|
||||
|
||||
const parts = [];
|
||||
// 动作失败的红字是状态而不是一次性注入:重绘(轮询、退出忙态的那一次)都不能把它擦掉。
|
||||
if (updateActionError) {
|
||||
parts.push('<div class="update-error">' + escapeHtml(updateActionError) + '</div>');
|
||||
}
|
||||
if (updateConsoleState.needsRestart) parts.push(renderRestartBanner());
|
||||
parts.push(updateSection(updateT('installTitle'), renderUpdateInstallBody(status)));
|
||||
parts.push(updateSection(updateT('sourceTitle'), renderUpdateSourceBody(status)));
|
||||
|
||||
Reference in new issue
Block a user