fix(updates): стабильный диалог установки, сохранение баннеров и логов без паразитных автозакрытий

This commit is contained in:
Dushnilin
2026-08-15 20:07:21 +03:00
parent d2a4243aa2
commit 91c7d5f8ee
5 changed files with 110 additions and 84 deletions
@@ -575,8 +575,11 @@ async function applyCompletedComponentAction({
}
if (notify) {
modalController?.completeSuccess(result.message, { reloadPage: true });
if (modalController) {
modalController.completeSuccess(result.message, { reloadPage: true });
} else {
reloadPageAfterTachyonUpdate();
}
} else {
modalController?.completeSuccess(result.message);
}
@@ -146,7 +146,7 @@ export function showUpdateProgressModal(
const logPreEl = E('pre', {
class: 'tachyon-update-modal__log',
}) as HTMLPreElement;
}, _('Operation started. Waiting for log output...')) as HTMLPreElement;
const logPanelEl = E('div', { class: 'tachyon-update-modal__log-panel' }, [
E('div', { class: 'tachyon-update-modal__log-header' }, [
@@ -205,8 +205,6 @@ export function showUpdateProgressModal(
let logPollTimer: ReturnType<typeof setTimeout> | null = null;
let logPollStopped = true;
let autoCompletedFromLog = false;
function appendLogText(text: string) {
if (!text) {
return;
@@ -214,26 +212,6 @@ export function showUpdateProgressModal(
logFullText += text;
logPreEl.textContent = logFullText;
logPreEl.scrollTop = logPreEl.scrollHeight;
if (
!autoCompletedFromLog &&
options.component === 'tachyon' &&
(options.action === 'install' || options.action === 'reinstall')
) {
if (
logFullText.includes('Tachyon updated to') ||
logFullText.includes('Start Tachyon')
) {
autoCompletedFromLog = true;
window.setTimeout(() => {
if (activeModalController === controller) {
controller.completeSuccess(_('Tachyon updated successfully!'), {
reloadPage: true,
});
}
}, 1000);
}
}
}
async function pollLog() {
@@ -338,21 +316,19 @@ export function showUpdateProgressModal(
? _('Check completed!')
: options.action === 'remove'
? _('Removal completed successfully!')
: _('Update completed successfully!'));
: _('Operation completed successfully!'));
actionButtonContainer.replaceChildren(
E('div', { class: 'tachyon-update-modal__success-banner' }, [
const bannerEl = E('div', { class: 'tachyon-update-modal__success-banner' }, [
renderCheckIcon24(),
E('span', {}, successMsg),
]),
);
]);
if (opts?.reloadPage) {
let secondsLeft = 3;
const reloadBtnText = () =>
`${_('Reloading page in')} ${secondsLeft}s...`;
const closeBtn = renderButton({
const reloadBtn = renderButton({
classNames: ['cbi-button-save'],
text: reloadBtnText(),
onClick: () => {
@@ -360,7 +336,14 @@ export function showUpdateProgressModal(
},
});
actionButtonContainer.replaceChildren(closeBtn);
actionButtonContainer.replaceChildren(
bannerEl,
E(
'div',
{ class: 'tachyon-update-modal__action-buttons' },
[reloadBtn],
),
);
const reloadTimer = setInterval(() => {
secondsLeft -= 1;
@@ -368,7 +351,7 @@ export function showUpdateProgressModal(
clearInterval(reloadTimer);
window.location.reload();
} else {
closeBtn.textContent = reloadBtnText();
reloadBtn.textContent = reloadBtnText();
}
}, 1000);
} else {
@@ -395,13 +378,20 @@ export function showUpdateProgressModal(
});
actionButtons.push(closeBtn);
actionButtonContainer.replaceChildren(...actionButtons);
actionButtonContainer.replaceChildren(
bannerEl,
E(
'div',
{ class: 'tachyon-update-modal__action-buttons' },
actionButtons,
),
);
const defaultAutoCloseMs = opts?.onInstall
? 0
: isCheckAction
? 1200
: 1200;
: 0;
const autoCloseMs = opts?.autoCloseMs ?? defaultAutoCloseMs;
if (autoCloseMs > 0) {
setTimeout(() => {
@@ -419,12 +409,10 @@ export function showUpdateProgressModal(
cleanupTimers();
finishLogTracking();
actionButtonContainer.replaceChildren(
E('div', { class: 'tachyon-update-modal__error-banner' }, [
const bannerEl = E('div', { class: 'tachyon-update-modal__error-banner' }, [
renderXIcon24(),
E('span', {}, errorMessage || _('Operation failed')),
]),
);
]);
const closeBtn = renderButton({
classNames: ['cbi-button-remove'],
@@ -434,7 +422,14 @@ export function showUpdateProgressModal(
},
});
actionButtonContainer.appendChild(closeBtn);
actionButtonContainer.replaceChildren(
bannerEl,
E(
'div',
{ class: 'tachyon-update-modal__action-buttons' },
[closeBtn],
),
);
},
startLogTracking: (jobId: string) => {
if (!jobId || logTrackingJobId === jobId) {
@@ -253,12 +253,20 @@ export const styles = `
.tachyon-update-modal__actions {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 6px;
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
padding-top: 10px;
flex-wrap: wrap;
}
.tachyon-update-modal__action-buttons {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.tachyon-update-modal__log-panel {
@@ -107,10 +107,11 @@ describe('renderUpdateProgressModal', () => {
vi.advanceTimersByTime(5000);
expect(mockUi.hideModal).not.toHaveBeenCalled();
controller.close();
vi.useRealTimers();
});
it('auto-closes after completed installation without hanging', async () => {
it('keeps completed installation open until user closes or autoCloseMs is explicit', async () => {
vi.useFakeTimers();
mockUi.showModal.mockReset();
mockUi.hideModal.mockReset();
@@ -127,8 +128,12 @@ describe('renderUpdateProgressModal', () => {
controller.completeSuccess('Update completed successfully!');
// Advance timer past autoCloseMs (1200ms)
vi.advanceTimersByTime(1500);
// Modal stays open so user can inspect log
vi.advanceTimersByTime(5000);
expect(mockUi.hideModal).not.toHaveBeenCalled();
// Explicit close works
controller.close();
expect(mockUi.hideModal).toHaveBeenCalled();
vi.useRealTimers();
@@ -16008,7 +16008,7 @@ function showUpdateProgressModal(options) {
]);
const logPreEl = E("pre", {
class: "tachyon-update-modal__log"
});
}, _("Operation started. Waiting for log output..."));
const logPanelEl = E("div", { class: "tachyon-update-modal__log-panel" }, [
E("div", { class: "tachyon-update-modal__log-header" }, [
E("b", {}, _("Operation log")),
@@ -16058,7 +16058,6 @@ function showUpdateProgressModal(options) {
let logFullText = "";
let logPollTimer = null;
let logPollStopped = true;
let autoCompletedFromLog = false;
function appendLogText(text) {
if (!text) {
return;
@@ -16066,18 +16065,6 @@ function showUpdateProgressModal(options) {
logFullText += text;
logPreEl.textContent = logFullText;
logPreEl.scrollTop = logPreEl.scrollHeight;
if (!autoCompletedFromLog && options.component === "tachyon" && (options.action === "install" || options.action === "reinstall")) {
if (logFullText.includes("Tachyon updated to") || logFullText.includes("Start Tachyon")) {
autoCompletedFromLog = true;
window.setTimeout(() => {
if (activeModalController === controller) {
controller.completeSuccess(_("Tachyon updated successfully!"), {
reloadPage: true
});
}
}, 1e3);
}
}
}
async function pollLog() {
if (logPollStopped || !logTrackingJobId) {
@@ -16155,31 +16142,36 @@ function showUpdateProgressModal(options) {
completeSuccess: (message, opts) => {
cleanupTimers();
finishLogTracking();
const successMsg = message || (isCheckAction ? _("Check completed!") : options.action === "remove" ? _("Removal completed successfully!") : _("Update completed successfully!"));
actionButtonContainer.replaceChildren(
E("div", { class: "tachyon-update-modal__success-banner" }, [
const successMsg = message || (isCheckAction ? _("Check completed!") : options.action === "remove" ? _("Removal completed successfully!") : _("Operation completed successfully!"));
const bannerEl = E("div", { class: "tachyon-update-modal__success-banner" }, [
renderCheckIcon24(),
E("span", {}, successMsg)
])
);
]);
if (opts?.reloadPage) {
let secondsLeft = 3;
const reloadBtnText = () => `${_("Reloading page in")} ${secondsLeft}s...`;
const closeBtn = renderButton({
const reloadBtn = renderButton({
classNames: ["cbi-button-save"],
text: reloadBtnText(),
onClick: () => {
window.location.reload();
}
});
actionButtonContainer.replaceChildren(closeBtn);
actionButtonContainer.replaceChildren(
bannerEl,
E(
"div",
{ class: "tachyon-update-modal__action-buttons" },
[reloadBtn]
)
);
const reloadTimer = setInterval(() => {
secondsLeft -= 1;
if (secondsLeft <= 0) {
clearInterval(reloadTimer);
window.location.reload();
} else {
closeBtn.textContent = reloadBtnText();
reloadBtn.textContent = reloadBtnText();
}
}, 1e3);
} else {
@@ -16203,8 +16195,15 @@ function showUpdateProgressModal(options) {
}
});
actionButtons.push(closeBtn);
actionButtonContainer.replaceChildren(...actionButtons);
const defaultAutoCloseMs = opts?.onInstall ? 0 : isCheckAction ? 1200 : 1200;
actionButtonContainer.replaceChildren(
bannerEl,
E(
"div",
{ class: "tachyon-update-modal__action-buttons" },
actionButtons
)
);
const defaultAutoCloseMs = opts?.onInstall ? 0 : isCheckAction ? 1200 : 0;
const autoCloseMs = opts?.autoCloseMs ?? defaultAutoCloseMs;
if (autoCloseMs > 0) {
setTimeout(() => {
@@ -16218,12 +16217,10 @@ function showUpdateProgressModal(options) {
completeError: (errorMessage) => {
cleanupTimers();
finishLogTracking();
actionButtonContainer.replaceChildren(
E("div", { class: "tachyon-update-modal__error-banner" }, [
const bannerEl = E("div", { class: "tachyon-update-modal__error-banner" }, [
renderXIcon24(),
E("span", {}, errorMessage || _("Operation failed"))
])
);
]);
const closeBtn = renderButton({
classNames: ["cbi-button-remove"],
text: _("Close"),
@@ -16231,7 +16228,14 @@ function showUpdateProgressModal(options) {
controller.close();
}
});
actionButtonContainer.appendChild(closeBtn);
actionButtonContainer.replaceChildren(
bannerEl,
E(
"div",
{ class: "tachyon-update-modal__action-buttons" },
[closeBtn]
)
);
},
startLogTracking: (jobId) => {
if (!jobId || logTrackingJobId === jobId) {
@@ -16630,8 +16634,11 @@ async function applyCompletedComponentAction({
showToast(result.message, "success", 1200);
}
if (notify) {
modalController?.completeSuccess(result.message, { reloadPage: true });
if (modalController) {
modalController.completeSuccess(result.message, { reloadPage: true });
} else {
reloadPageAfterTachyonUpdate();
}
} else {
modalController?.completeSuccess(result.message);
}
@@ -17696,12 +17703,20 @@ var styles6 = `
.tachyon-update-modal__actions {
display: flex;
justify-content: flex-end;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 6px;
border-top: 1px solid var(--border-color, rgba(255,255,255,0.12));
padding-top: 10px;
flex-wrap: wrap;
}
.tachyon-update-modal__action-buttons {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.tachyon-update-modal__log-panel {