diff --git a/app/views/install/installer/js/modules/progress.js b/app/views/install/installer/js/modules/progress.js index 162ebd0aa0..bb23c773a2 100644 --- a/app/views/install/installer/js/modules/progress.js +++ b/app/views/install/installer/js/modules/progress.js @@ -279,6 +279,8 @@ const redirectToApp = () => { const url = buildRedirectUrl(); if (!url) return; + // Fire-and-forget: tell the installer server it can shut down + fetch('/install/shutdown', { method: 'POST', headers: withCsrfHeader() }).catch(() => {}); window.location.href = url; }; @@ -422,9 +424,6 @@ list.innerHTML = ''; const rowsById = new Map(); const progressState = new Map(); - let pendingProgressTimer = null; - let pendingProgressStep = null; - let pendingCompletionTimer = null; syncInstallLockFlag?.(); applyLockPayload?.(); applyBodyDefaults?.(); @@ -554,15 +553,6 @@ return; } - if (pendingProgressTimer && pendingProgressStep === payload.step && payload.status !== STATUS.IN_PROGRESS) { - clearTimeout(pendingProgressTimer); - pendingProgressTimer = null; - pendingProgressStep = null; - } - if (pendingCompletionTimer) { - clearTimeout(pendingCompletionTimer); - pendingCompletionTimer = null; - } const step = getStepDefinition(payload.step) || { id: payload.step, inProgress: payload.message || payload.step, @@ -570,40 +560,19 @@ }; if (payload.status === STATUS.IN_PROGRESS) { const currentIndex = INSTALLATION_STEPS.findIndex((candidate) => candidate.id === step.id); - const completionTargets = []; if (currentIndex > 0) { for (let i = 0; i < currentIndex; i += 1) { const previousStep = INSTALLATION_STEPS[i]; const previousState = progressState.get(previousStep.id); if (previousState && previousState.status !== STATUS.COMPLETED) { - completionTargets.push({ - step: previousStep, + progressState.set(previousStep.id, { + status: STATUS.COMPLETED, + message: previousStep.done, details: previousState.details }); } } } - if (completionTargets.length && currentIndex > 0) { - pendingProgressStep = payload.step; - pendingCompletionTimer = setTimeout(() => { - pendingCompletionTimer = null; - completionTargets.forEach(({ step: previousStep, details }) => { - progressState.set(previousStep.id, { - status: STATUS.COMPLETED, - message: previousStep.done, - details - }); - }); - renderProgress(); - }, TIMINGS?.progressCompleteDelay ?? 0); - pendingProgressTimer = setTimeout(() => { - pendingProgressTimer = null; - pendingProgressStep = null; - applyProgress(payload); - }, TIMINGS?.progressTransitionDelay ?? 0); - scheduleFallback(); - return; - } } applyProgress(payload); }; @@ -622,6 +591,19 @@ renderProgress(); }; + const checkAllCompleted = () => { + const allDone = INSTALLATION_STEPS.every((step) => { + const state = progressState.get(step.id); + return state && state.status === STATUS.COMPLETED; + }); + if (!allDone) return; + const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); + finalizeInstall(); + notifyInstallComplete(activeInstall?.installId, accountState?.details).finally(() => { + setTimeout(() => redirectToApp(), TIMINGS?.redirectDelay ?? 0); + }); + }; + const startPolling = () => { if (!activeInstall || activeInstall.pollTimer) return; activeInstall.pollTimer = setInterval(async () => { @@ -629,6 +611,7 @@ const snapshot = await fetchInstallStatus(activeInstall.installId); if (snapshot) { applySnapshot(snapshot); + checkAllCompleted(); } }, TIMINGS?.installPollInterval ?? 0); }; @@ -726,18 +709,20 @@ return; } if (event === SSE_EVENTS.DONE) { - const lastStep = INSTALLATION_STEPS[INSTALLATION_STEPS.length - 1]; - if (lastStep) { - const lastState = progressState.get(lastStep.id); - if (!lastState || (lastState.status !== STATUS.COMPLETED && lastState.status !== STATUS.ERROR)) { - progressState.set(lastStep.id, { - status: STATUS.COMPLETED, - message: lastStep.done, - details: lastState?.details - }); - renderProgress(); - } - } + // Mark every step as completed (preserving details + // from earlier progress events, e.g. session info). + INSTALLATION_STEPS.forEach((step) => { + const existing = progressState.get(step.id); + if (!existing || (existing.status !== STATUS.COMPLETED && existing.status !== STATUS.ERROR)) { + progressState.set(step.id, { + status: STATUS.COMPLETED, + message: step.done, + details: existing?.details + }); + } + }); + renderProgress(); + const accountState = progressState.get(STEP_IDS.ACCOUNT_SETUP); const accountRequired = INSTALLATION_STEPS.some((step) => step.id === STEP_IDS.ACCOUNT_SETUP); if (accountRequired && accountState?.status === STATUS.ERROR) { @@ -786,7 +771,12 @@ } }); if (activeInstall && !activeInstall.completed) { - startPolling(); + // Stream ended without a "done" event (e.g. browser + // throttled the background tab). Check if we're done. + checkAllCompleted(); + if (!activeInstall?.completed) { + startPolling(); + } } } catch (error) { if (!activeInstall || activeInstall.controller.signal.aborted) { @@ -872,6 +862,14 @@ } }); + // When the user switches back to this tab, check if installation + // finished while the tab was in the background. + document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible' && activeInstall && !activeInstall.completed) { + checkAllCompleted(); + } + }); + const lock = getInstallLock?.(); const existingInstallId = lock?.installId || getStoredInstallId?.(); if (existingInstallId) {