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