diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts b/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts index 4490f96b..6a3e63c5 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/initController.ts @@ -575,8 +575,11 @@ async function applyCompletedComponentAction({ } if (notify) { - modalController?.completeSuccess(result.message, { reloadPage: true }); - reloadPageAfterTachyonUpdate(); + if (modalController) { + modalController.completeSuccess(result.message, { reloadPage: true }); + } else { + reloadPageAfterTachyonUpdate(); + } } else { modalController?.completeSuccess(result.message); } diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts b/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts index 1e567263..26733c28 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/partials/renderUpdateProgressModal.ts @@ -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 | 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' }, [ - renderCheckIcon24(), - E('span', {}, successMsg), - ]), - ); + 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' }, [ - renderXIcon24(), - E('span', {}, errorMessage || _('Operation failed')), - ]), - ); + 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) { diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts b/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts index 80d6ca0e..002c003b 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/styles.ts @@ -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 { diff --git a/fe-app-tachyon/src/tachyon/tabs/updates/tests/updateProgressModal.test.ts b/fe-app-tachyon/src/tachyon/tabs/updates/tests/updateProgressModal.test.ts index a35752da..3d1325dc 100644 --- a/fe-app-tachyon/src/tachyon/tabs/updates/tests/updateProgressModal.test.ts +++ b/fe-app-tachyon/src/tachyon/tabs/updates/tests/updateProgressModal.test.ts @@ -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(); diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index 2e3dab3e..bc2bf77b 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -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" }, [ - renderCheckIcon24(), - E("span", {}, successMsg) - ]) - ); + 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" }, [ - renderXIcon24(), - E("span", {}, errorMessage || _("Operation failed")) - ]) - ); + 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 }); - reloadPageAfterTachyonUpdate(); + 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 {